Custom Fields: Harden implementation closes #194 #204

Merged
carla merged 5 commits from feature/harden-custom-fields into main 2025-11-17 17:01:32 +01:00
39 changed files with 2337 additions and 1269 deletions

View file

@ -81,8 +81,8 @@ lib/
├── membership/ # Membership domain ├── membership/ # Membership domain
│ ├── membership.ex # Domain definition │ ├── membership.ex # Domain definition
│ ├── member.ex # Member resource │ ├── member.ex # Member resource
│ ├── property.ex # Custom property resource │ ├── custom_field_value.ex # Custom field value resource
│ ├── property_type.ex # Property type resource │ ├── custom_field.ex # CustomFieldValue type resource
│ └── email.ex # Email custom type │ └── email.ex # Email custom type
├── mv/ # Core application modules ├── mv/ # Core application modules
│ ├── accounts/ # Domain-specific logic │ ├── accounts/ # Domain-specific logic
@ -121,8 +121,8 @@ lib/
│ │ │ ├── search_bar_component.ex │ │ │ ├── search_bar_component.ex
│ │ │ └── sort_header_component.ex │ │ │ └── sort_header_component.ex
│ │ ├── member_live/ # Member CRUD LiveViews │ │ ├── member_live/ # Member CRUD LiveViews
│ │ ├── property_live/ # Property CRUD LiveViews │ │ ├── custom_field_value_live/ # CustomFieldValue CRUD LiveViews
│ │ ├── property_type_live/ │ │ ├── custom_field_live/
│ │ └── user_live/ # User management LiveViews │ │ └── user_live/ # User management LiveViews
│ ├── auth_overrides.ex # AshAuthentication overrides │ ├── auth_overrides.ex # AshAuthentication overrides
│ ├── endpoint.ex # Phoenix endpoint │ ├── endpoint.ex # Phoenix endpoint
@ -740,14 +740,14 @@ end
# Good - preload relationships # Good - preload relationships
members = members =
Member Member
|> Ash.Query.load(:properties) |> Ash.Query.load(:custom_field_values)
|> Mv.Membership.list_members!() |> Mv.Membership.list_members!()
# Avoid - causes N+1 queries # Avoid - causes N+1 queries
members = Mv.Membership.list_members!() members = Mv.Membership.list_members!()
Enum.map(members, fn member -> Enum.map(members, fn member ->
# This triggers a query for each member # This triggers a query for each member
Ash.load!(member, :properties) Ash.load!(member, :custom_field_values)
end) end)
``` ```
@ -1723,13 +1723,13 @@ end
# Good - preload relationships # Good - preload relationships
members = members =
Member Member
|> Ash.Query.load([:properties, :user]) |> Ash.Query.load([:custom_field_values, :user])
|> Mv.Membership.list_members!() |> Mv.Membership.list_members!()
# Avoid - causes N+1 # Avoid - causes N+1
members = Mv.Membership.list_members!() members = Mv.Membership.list_members!()
Enum.map(members, fn member -> Enum.map(members, fn member ->
properties = Ash.load!(member, :properties) # N queries! custom_field_values = Ash.load!(member, :custom_field_values) # N queries!
end) end)
``` ```
@ -1904,7 +1904,7 @@ defmodule Mv.Membership.Member do
@moduledoc """ @moduledoc """
Represents a club member with their personal information and membership status. Represents a club member with their personal information and membership status.
Members can have custom properties defined by the club administrators. Members can have custom_field_values defined by the club administrators.
Each member is optionally linked to a user account for self-service access. Each member is optionally linked to a user account for self-service access.
## Examples ## Examples
@ -2050,7 +2050,7 @@ open doc/index.html
## [Unreleased] ## [Unreleased]
### Added ### Added
- Member custom properties feature - Member custom_field_values feature
- Email synchronization between user and member - Email synchronization between user and member
### Changed ### Changed
@ -2081,14 +2081,14 @@ open doc/index.html
```bash ```bash
# Create feature branch # Create feature branch
git checkout -b feature/member-custom-properties git checkout -b feature/member-custom-custom_field_values
# Work on feature # Work on feature
git add . git add .
git commit -m "Add custom properties to members" git commit -m "Add custom_field_values to members"
# Push to remote # Push to remote
git push origin feature/member-custom-properties git push origin feature/member-custom-custom_field_values
``` ```
### 8.2 Commit Messages ### 8.2 Commit Messages
@ -2127,7 +2127,7 @@ Closes #123
``` ```
fix: resolve N+1 query in member list fix: resolve N+1 query in member list
Preload properties relationship when loading members to avoid N+1 queries. Preload custom_field_values relationship when loading members to avoid N+1 queries.
Performance improvement: reduced query count from 100+ to 2. Performance improvement: reduced query count from 100+ to 2.
``` ```

View file

@ -52,21 +52,21 @@ This document provides a comprehensive overview of the Mila Membership Managemen
- Bidirectional email sync with users - Bidirectional email sync with users
- Flexible address and contact data - Flexible address and contact data
#### `properties` #### `custom_field_values`
- **Purpose:** Dynamic custom member attributes - **Purpose:** Dynamic custom member attributes
- **Rows (Estimated):** Variable (N per member) - **Rows (Estimated):** Variable (N per member)
- **Key Features:** - **Key Features:**
- Union type value storage (JSONB) - Union type value storage (JSONB)
- Multiple data types supported - Multiple data types supported
- One property per type per member - One custom field value per custom field per member
#### `property_types` #### `custom_fields`
- **Purpose:** Schema definitions for custom properties - **Purpose:** Schema definitions for custom_field_values
- **Rows (Estimated):** Low (admin-defined) - **Rows (Estimated):** Low (admin-defined)
- **Key Features:** - **Key Features:**
- Type definitions - Type definitions
- Immutable and required flags - Immutable and required flags
- Centralized property management - Centralized custom field management
## Key Relationships ## Key Relationships
@ -77,7 +77,7 @@ User (0..1) ←→ (0..1) Member
Member (1) → (N) Properties Member (1) → (N) Properties
PropertyType (1) CustomField (1)
``` ```
### Relationship Details ### Relationship Details
@ -90,11 +90,11 @@ Member (1) → (N) Properties
- `ON DELETE SET NULL` on user side (User preserved when Member deleted) - `ON DELETE SET NULL` on user side (User preserved when Member deleted)
2. **Member → Properties (1:N)** 2. **Member → Properties (1:N)**
- One member, many properties - One member, many custom_field_values
- `ON DELETE CASCADE` - properties deleted with member - `ON DELETE CASCADE` - custom_field_values deleted with member
- Composite unique constraint (member_id, property_type_id) - Composite unique constraint (member_id, custom_field_id)
3. **Property → PropertyType (N:1)** 3. **CustomFieldValue → CustomField (N:1)**
- Properties reference type definition - Properties reference type definition
- `ON DELETE RESTRICT` - cannot delete type if in use - `ON DELETE RESTRICT` - cannot delete type if in use
- Type defines data structure - Type defines data structure
@ -121,8 +121,8 @@ Member (1) → (N) Properties
- Phone: `+?[0-9\- ]{6,20}` - Phone: `+?[0-9\- ]{6,20}`
- Postal code: 5 digits - Postal code: 5 digits
### Property System ### CustomFieldValue System
- Maximum one property per type per member - Maximum one custom field value per custom field per member
moritz marked this conversation as resolved

We would not think about enums here, right? Because in case of that or lists we would have more than one value per custom field...

We would not think about enums here, right? Because in case of that or lists we would have more than one value per custom field...

Enums will just enable a selection of different values. But inside the custom field value only one value will be stored.

Enums will just enable a selection of different values. But inside the `custom field value` only one value will be stored.
- Value stored as union type in JSONB - Value stored as union type in JSONB
- Supported types: string, integer, boolean, date, email - Supported types: string, integer, boolean, date, email
- Types can be marked as immutable or required - Types can be marked as immutable or required
@ -144,10 +144,10 @@ Member (1) → (N) Properties
- `join_date` (B-tree) - Date filtering - `join_date` (B-tree) - Date filtering
- `paid` (partial B-tree) - Payment status queries - `paid` (partial B-tree) - Payment status queries
**properties:** **custom_field_values:**
- `member_id` - Member property lookups - `member_id` - Member custom field value lookups
- `property_type_id` - Type-based queries - `custom_field_id` - Type-based queries
- Composite `(member_id, property_type_id)` - Uniqueness - Composite `(member_id, custom_field_id)` - Uniqueness
**tokens:** **tokens:**
- `subject` - User token lookups - `subject` - User token lookups
@ -297,8 +297,8 @@ priv/repo/migrations/
| Relationship | On Delete | Rationale | | Relationship | On Delete | Rationale |
|--------------|-----------|-----------| |--------------|-----------|-----------|
| `users.member_id → members.id` | SET NULL | Preserve user account when member deleted | | `users.member_id → members.id` | SET NULL | Preserve user account when member deleted |
| `properties.member_id → members.id` | CASCADE | Delete properties with member | | `custom_field_values.member_id → members.id` | CASCADE | Delete custom_field_values with member |
| `properties.property_type_id → property_types.id` | RESTRICT | Prevent deletion of types in use | | `custom_field_values.custom_field_id → custom_fields.id` | RESTRICT | Prevent deletion of types in use |
### Validation Layers ### Validation Layers
@ -327,15 +327,15 @@ priv/repo/migrations/
- Member search (uses GIN index on search_vector) - Member search (uses GIN index on search_vector)
- Member list with filters (uses indexes on join_date, paid) - Member list with filters (uses indexes on join_date, paid)
- User authentication (uses unique index on email/oidc_id) - User authentication (uses unique index on email/oidc_id)
- Property lookups by member (uses index on member_id) - CustomFieldValue lookups by member (uses index on member_id)
**Medium Frequency:** **Medium Frequency:**
- Member CRUD operations - Member CRUD operations
- Property updates - CustomFieldValue updates
- Token validation - Token validation
**Low Frequency:** **Low Frequency:**
- PropertyType management - CustomField management
- User-Member linking - User-Member linking
- Bulk operations - Bulk operations
@ -396,10 +396,10 @@ Install "DBML Language" extension to view/edit DBML files with:
### Critical Tables (Priority 1) ### Critical Tables (Priority 1)
- `members` - Core business data - `members` - Core business data
- `users` - Authentication data - `users` - Authentication data
- `property_types` - Schema definitions - `custom_fields` - Schema definitions
### Important Tables (Priority 2) ### Important Tables (Priority 2)
- `properties` - Member custom data - `custom_field_values` - Member custom data
- `tokens` - Can be regenerated but good to backup - `tokens` - Can be regenerated but good to backup
### Backup Strategy ### Backup Strategy

View file

@ -18,7 +18,7 @@ Project mila_membership_management {
## Key Features: ## Key Features:
- User authentication (OIDC + Password with secure account linking) - User authentication (OIDC + Password with secure account linking)
- Member management with flexible custom properties - Member management with flexible custom fields
- Bidirectional email synchronization between users and members - Bidirectional email synchronization between users and members
- Full-text search capabilities (tsvector) - Full-text search capabilities (tsvector)
- Fuzzy search with trigram matching (pg_trgm) - Fuzzy search with trigram matching (pg_trgm)
@ -26,7 +26,7 @@ Project mila_membership_management {
## Domains: ## Domains:
- **Accounts**: User authentication and session management - **Accounts**: User authentication and session management
- **Membership**: Club member data and custom properties - **Membership**: Club member data and custom fields
## Required PostgreSQL Extensions: ## Required PostgreSQL Extensions:
- uuid-ossp (UUID generation) - uuid-ossp (UUID generation)
@ -178,7 +178,7 @@ Table members {
**Relationships:** **Relationships:**
- Optional 1:1 with users (0..1 ↔ 0..1) - authentication account - Optional 1:1 with users (0..1 ↔ 0..1) - authentication account
- 1:N with properties (custom dynamic fields) - 1:N with custom_field_values (custom dynamic fields)
**Validation Rules:** **Validation Rules:**
- first_name, last_name: min 1 character - first_name, last_name: min 1 character
@ -191,20 +191,20 @@ Table members {
''' '''
} }
Table properties { Table custom_field_values {
id uuid [pk, not null, default: `gen_random_uuid()`, note: 'Primary identifier'] id uuid [pk, not null, default: `gen_random_uuid()`, note: 'Primary identifier']
value jsonb [null, note: 'Union type value storage (format: {type: "string", value: "example"})'] value jsonb [null, note: 'Union type value storage (format: {type: "string", value: "example"})']
member_id uuid [not null, note: 'Link to member'] member_id uuid [not null, note: 'Link to member']
property_type_id uuid [not null, note: 'Link to property type definition'] custom_field_id uuid [not null, note: 'Link to custom field definition']
indexes { indexes {
(member_id, property_type_id) [unique, name: 'properties_unique_property_per_member_index', note: 'One property per type per member'] (member_id, custom_field_id) [unique, name: 'custom_field_values_unique_custom_field_per_member_index', note: 'One custom field value per custom field per member']
member_id [name: 'properties_member_id_idx'] member_id [name: 'custom_field_values_member_id_idx']
property_type_id [name: 'properties_property_type_id_idx'] custom_field_id [name: 'custom_field_values_custom_field_id_idx']
} }
Note: ''' Note: '''
**Dynamic Custom Member Properties** **Dynamic Custom Member Field Values**
Provides flexible, extensible attributes for members beyond the fixed schema. Provides flexible, extensible attributes for members beyond the fixed schema.
@ -221,9 +221,9 @@ Table properties {
- `email`: Validated email addresses - `email`: Validated email addresses
**Constraints:** **Constraints:**
- Each member can have only ONE property per property_type - Each member can have only ONE custom field value per custom field
- Properties are deleted when member is deleted (CASCADE) - Custom field values are deleted when member is deleted (CASCADE)
- Property type cannot be deleted if properties exist (RESTRICT) - Custom field cannot be deleted if custom field values exist (RESTRICT)
**Use Cases:** **Use Cases:**
- Custom membership numbers - Custom membership numbers
@ -233,34 +233,34 @@ Table properties {
''' '''
} }
Table property_types { Table custom_fields {
id uuid [pk, not null, default: `gen_random_uuid()`, note: 'Primary identifier'] id uuid [pk, not null, default: `gen_random_uuid()`, note: 'Primary identifier']
name text [not null, unique, note: 'Property name/identifier (e.g., "membership_number")'] name text [not null, unique, note: 'CustomFieldValue name/identifier (e.g., "membership_number")']
value_type text [not null, note: 'Data type: string | integer | boolean | date | email'] value_type text [not null, note: 'Data type: string | integer | boolean | date | email']
description text [null, note: 'Human-readable description'] description text [null, note: 'Human-readable description']
immutable boolean [not null, default: false, note: 'If true, value cannot be changed after creation'] immutable boolean [not null, default: false, note: 'If true, value cannot be changed after creation']
required boolean [not null, default: false, note: 'If true, all members must have this property'] required boolean [not null, default: false, note: 'If true, all members must have this custom field']
indexes { indexes {
name [unique, name: 'property_types_unique_name_index'] name [unique, name: 'custom_fields_unique_name_index']
} }
Note: ''' Note: '''
**Property Type Definitions** **CustomFieldValue Type Definitions**
Defines the schema and behavior for custom member properties. Defines the schema and behavior for custom member custom_field_values.
**Attributes:** **Attributes:**
- `name`: Unique identifier for the property type - `name`: Unique identifier for the custom field
- `value_type`: Enforces data type consistency - `value_type`: Enforces data type consistency
- `description`: Documentation for users/admins - `description`: Documentation for users/admins
- `immutable`: Prevents changes after initial creation (e.g., membership numbers) - `immutable`: Prevents changes after initial creation (e.g., membership numbers)
- `required`: Enforces that all members must have this property - `required`: Enforces that all members must have this custom field
**Constraints:** **Constraints:**
- `value_type` must be one of: string, integer, boolean, date, email - `value_type` must be one of: string, integer, boolean, date, email
- `name` must be unique across all property types - `name` must be unique across all custom fields
- Cannot be deleted if properties reference it (ON DELETE RESTRICT) - Cannot be deleted if custom_field_values reference it (ON DELETE RESTRICT)
**Examples:** **Examples:**
- Membership Number (string, immutable, required) - Membership Number (string, immutable, required)
@ -283,25 +283,25 @@ Table property_types {
Ref: users.member_id - members.id [delete: set null] Ref: users.member_id - members.id [delete: set null]
// Member → Properties (1:N) // Member → Properties (1:N)
// - One member can have multiple properties // - One member can have multiple custom_field_values
// - Each property belongs to exactly one member // - Each custom field value belongs to exactly one member
// - ON DELETE CASCADE: Properties deleted when member deleted // - ON DELETE CASCADE: Properties deleted when member deleted
// - UNIQUE constraint: One property per type per member // - UNIQUE constraint: One custom field value per custom field per member
Ref: properties.member_id > members.id [delete: cascade] Ref: custom_field_values.member_id > members.id [delete: cascade]
// Property → PropertyType (N:1) // CustomFieldValue → CustomField (N:1)
// - Many properties can reference one property type // - Many custom_field_values can reference one custom field
// - Property type defines the schema/behavior // - CustomFieldValue type defines the schema/behavior
// - ON DELETE RESTRICT: Cannot delete type if properties exist // - ON DELETE RESTRICT: Cannot delete type if custom_field_values exist
Ref: properties.property_type_id > property_types.id [delete: restrict] Ref: custom_field_values.custom_field_id > custom_fields.id [delete: restrict]
// ============================================ // ============================================
// ENUMS // ENUMS
// ============================================ // ============================================
// Valid data types for property values // Valid data types for custom field values
// Determines how Property.value is interpreted // Determines how CustomFieldValue.value is interpreted
Enum property_value_type { Enum custom_field_value_type {
string [note: 'Text data'] string [note: 'Text data']
integer [note: 'Numeric data'] integer [note: 'Numeric data']
boolean [note: 'True/False flags'] boolean [note: 'True/False flags']
@ -335,8 +335,8 @@ TableGroup accounts_domain {
TableGroup membership_domain { TableGroup membership_domain {
members members
properties custom_field_values
property_types custom_fields
Note: ''' Note: '''
**Membership Domain** **Membership Domain**

View file

@ -131,11 +131,11 @@ Based on closed PRs from https://git.local-it.org/local-it/mitgliederverwaltung/
**Sprint 3 - 28.05 - 09.07** **Sprint 3 - 28.05 - 09.07**
- Member CRUD operations - Member CRUD operations
- Basic property system - Basic custom field system
- Initial UI with Tailwind CSS - Initial UI with Tailwind CSS
**Sprint 4 - 09.07 - 30.07** **Sprint 4 - 09.07 - 30.07**
- Property types implementation - CustomFieldValue types implementation
- Data validation - Data validation
- Error handling improvements - Error handling improvements
@ -154,7 +154,7 @@ Based on closed PRs from https://git.local-it.org/local-it/mitgliederverwaltung/
**PR #147:** *Add seed data for members* **PR #147:** *Add seed data for members*
- Comprehensive seed data - Comprehensive seed data
- Test users and members - Test users and members
- Property type examples - CustomFieldValue type examples
#### Phase 3: Search & Navigation (Sprint 6) #### Phase 3: Search & Navigation (Sprint 6)
@ -379,21 +379,21 @@ end
**Complete documentation:** See [`docs/email-sync.md`](email-sync.md) for decision tree and sync rules. **Complete documentation:** See [`docs/email-sync.md`](email-sync.md) for decision tree and sync rules.
#### 4. Property System (EAV Pattern) #### 4. CustomFieldValue System (EAV Pattern)
**Implementation:** Entity-Attribute-Value pattern with union types **Implementation:** Entity-Attribute-Value pattern with union types
```elixir ```elixir
# Property Type defines schema # CustomFieldValue Type defines schema
defmodule Mv.Membership.PropertyType do defmodule Mv.Membership.CustomField do
attribute :name, :string # "Membership Number" attribute :name, :string # "Membership Number"
attribute :value_type, :atom # :string, :integer, :boolean, :date, :email attribute :value_type, :atom # :string, :integer, :boolean, :date, :email
attribute :immutable, :boolean # Can't change after creation attribute :immutable, :boolean # Can't change after creation
attribute :required, :boolean # All members must have this attribute :required, :boolean # All members must have this
end end
# Property stores values # CustomFieldValue stores values
defmodule Mv.Membership.Property do defmodule Mv.Membership.CustomFieldValue do
attribute :value, :union, # Polymorphic value storage attribute :value, :union, # Polymorphic value storage
constraints: [ constraints: [
types: [ types: [
@ -405,7 +405,7 @@ defmodule Mv.Membership.Property do
] ]
] ]
belongs_to :member belongs_to :member
belongs_to :property_type belongs_to :custom_field
end end
``` ```
@ -413,12 +413,12 @@ end
- Clubs need different custom fields - Clubs need different custom fields
- No schema migrations for new fields - No schema migrations for new fields
- Type safety with union types - Type safety with union types
- Centralized property management - Centralized custom field management
**Constraints:** **Constraints:**
- One property per type per member (composite unique index) - One custom field value per custom field per member (composite unique index)
- Properties deleted with member (CASCADE) - Properties deleted with member (CASCADE)
- Property types protected if in use (RESTRICT) - CustomFieldValue types protected if in use (RESTRICT)
#### 5. Authentication Strategy #### 5. Authentication Strategy
@ -593,7 +593,7 @@ end
#### Database Migrations #### Database Migrations
**Key migrations in chronological order:** **Key migrations in chronological order:**
1. `20250528163901_initial_migration.exs` - Core tables (members, properties, property_types) 1. `20250528163901_initial_migration.exs` - Core tables (members, custom_field_values, custom_fields)
2. `20250617090641_member_fields.exs` - Member attributes expansion 2. `20250617090641_member_fields.exs` - Member attributes expansion
3. `20250620110850_add_accounts_domain.exs` - Users & tokens tables 3. `20250620110850_add_accounts_domain.exs` - Users & tokens tables
4. `20250912085235_AddSearchVectorToMembers.exs` - Full-text search (tsvector + GIN index) 4. `20250912085235_AddSearchVectorToMembers.exs` - Full-text search (tsvector + GIN index)
@ -772,7 +772,7 @@ end
- Admin user: `admin@mv.local` / `testpassword` - Admin user: `admin@mv.local` / `testpassword`
- Sample members: Hans Müller, Greta Schmidt, Friedrich Wagner - Sample members: Hans Müller, Greta Schmidt, Friedrich Wagner
- Linked accounts: Maria Weber, Thomas Klein - Linked accounts: Maria Weber, Thomas Klein
- Property types: String, Date, Boolean, Email - CustomFieldValue types: String, Date, Boolean, Email
**Test Helpers:** **Test Helpers:**
```elixir ```elixir
@ -956,9 +956,9 @@ mix credo --strict
mix credo suggest --format=oneline mix credo suggest --format=oneline
``` ```
### 8. Property Value Type Mismatch ### 8. CustomFieldValue Value Type Mismatch
**Issue:** Property value doesn't match property_type definition. **Issue:** CustomFieldValue value doesn't match custom_field definition.
**Error:** **Error:**
``` ```
@ -966,16 +966,16 @@ mix credo suggest --format=oneline
``` ```
**Solution:** **Solution:**
Ensure property value matches property_type.value_type: Ensure custom field value matches custom_field.value_type:
```elixir ```elixir
# Property Type: value_type = :integer # CustomFieldValue Type: value_type = :integer
property_type = get_property_type("age") custom_field = get_custom_field("age")
# Property Value: must be integer union type # CustomFieldValue Value: must be integer union type
{:ok, property} = create_property(%{ {:ok, custom_field_value} = create_custom_field_value(%{
value: %{type: :integer, value: 25}, # Not "25" as string value: %{type: :integer, value: 25}, # Not "25" as string
property_type_id: property_type.id custom_field_id: custom_field.id
}) })
``` ```

View file

@ -87,12 +87,12 @@
--- ---
#### 3. **Custom Fields (Property System)** 🔧 #### 3. **Custom Fields (CustomFieldValue System)** 🔧
**Current State:** **Current State:**
- ✅ Property types (string, integer, boolean, date, email) - ✅ CustomFieldValue types (string, integer, boolean, date, email)
- ✅ Property type management - ✅ CustomFieldValue type management
- ✅ Dynamic property assignment to members - ✅ Dynamic custom field value assignment to members
- ✅ Union type storage (JSONB) - ✅ Union type storage (JSONB)
**Open Issues:** **Open Issues:**
@ -217,7 +217,7 @@
- ❌ Global settings management - ❌ Global settings management
- ❌ Club/Organization profile - ❌ Club/Organization profile
- ❌ Email templates configuration - ❌ Email templates configuration
- ❌ Property type management UI (user-facing) - ❌ CustomFieldValue type management UI (user-facing)
- ❌ Role and permission management UI - ❌ Role and permission management UI
- ❌ System health dashboard - ❌ System health dashboard
- ❌ Audit log viewer - ❌ Audit log viewer
@ -481,9 +481,9 @@ Since this is a **Phoenix LiveView** application with **Ash Framework**, we have
| Mount | Purpose | Auth | Query Params | Events | | Mount | Purpose | Auth | Query Params | Events |
|-------|---------|------|--------------|--------| |-------|---------|------|--------------|--------|
| `/members` | Member list with search/sort | 🔐 | `?search=&sort_by=&sort_dir=` | `search`, `sort`, `delete`, `select` | | `/members` | Member list with search/sort | 🔐 | `?search=&sort_by=&sort_dir=` | `search`, `sort`, `delete`, `select` |
| `/members/new` | Create new member form | 🔐 | - | `save`, `cancel`, `add_property` | | `/members/new` | Create new member form | 🔐 | - | `save`, `cancel`, `add_custom_field_value` |
| `/members/:id` | Member detail view | 🔐 | - | `edit`, `delete`, `link_user` | | `/members/:id` | Member detail view | 🔐 | - | `edit`, `delete`, `link_user` |
| `/members/:id/edit` | Edit member form | 🔐 | - | `save`, `cancel`, `add_property`, `remove_property` | | `/members/:id/edit` | Edit member form | 🔐 | - | `save`, `cancel`, `add_custom_field_value`, `remove_custom_field_value` |
#### LiveView Event Handlers #### LiveView Event Handlers
@ -495,8 +495,8 @@ Since this is a **Phoenix LiveView** application with **Ash Framework**, we have
| `save` | Create/update member | `%{"member" => attrs}` | Redirect or show errors | | `save` | Create/update member | `%{"member" => attrs}` | Redirect or show errors |
| `link_user` | Link user to member | `%{"user_id" => id}` | Update member view | | `link_user` | Link user to member | `%{"user_id" => id}` | Update member view |
| `unlink_user` | Unlink user from member | - | Update member view | | `unlink_user` | Unlink user from member | - | Update member view |
| `add_property` | Add custom property | `%{"property_type_id" => id, "value" => val}` | Update form | | `add_custom_field_value` | Add custom field value | `%{"custom_field_id" => id, "value" => val}` | Update form |
| `remove_property` | Remove custom property | `%{"property_id" => id}` | Update form | | `remove_custom_field_value` | Remove custom field value | `%{"custom_field_value_id" => id}` | Update form |
#### Ash Resource Actions #### Ash Resource Actions
@ -517,7 +517,7 @@ Since this is a **Phoenix LiveView** application with **Ash Framework**, we have
| `Member` | `:fuzzy_search` | Fuzzy text search | 🔐 | `{query, threshold}` | `[%Member{}]` | | `Member` | `:fuzzy_search` | Fuzzy text search | 🔐 | `{query, threshold}` | `[%Member{}]` |
| `Member` | `:advanced_search` | Multi-criteria search | 🔐 | `{filters: [{field, op, value}]}` | `[%Member{}]` | | `Member` | `:advanced_search` | Multi-criteria search | 🔐 | `{filters: [{field, op, value}]}` | `[%Member{}]` |
| `Member` | `:paginate` | Paginated member list | 🔐 | `{page, per_page, filters}` | `{members, total, page_info}` | | `Member` | `:paginate` | Paginated member list | 🔐 | `{page, per_page, filters}` | `{members, total, page_info}` |
| `Member` | `:sort_by_custom_field` | Sort by property | 🔐 | `{property_type_id, direction}` | `[%Member{}]` | | `Member` | `:sort_by_custom_field` | Sort by custom field | 🔐 | `{custom_field_id, direction}` | `[%Member{}]` |
| `Member` | `:bulk_delete` | Delete multiple members | 🛡️ | `{ids: [id1, id2, ...]}` | `{:ok, count}` | | `Member` | `:bulk_delete` | Delete multiple members | 🛡️ | `{ids: [id1, id2, ...]}` | `{:ok, count}` |
| `Member` | `:bulk_update` | Update multiple members | 🛡️ | `{ids, attrs}` | `{:ok, count}` | | `Member` | `:bulk_update` | Update multiple members | 🛡️ | `{ids, attrs}` | `{:ok, count}` |
| `Member` | `:export` | Export to CSV/Excel | 🔐 | `{format, filters}` | File download | | `Member` | `:export` | Export to CSV/Excel | 🔐 | `{format, filters}` | File download |
@ -525,37 +525,37 @@ Since this is a **Phoenix LiveView** application with **Ash Framework**, we have
--- ---
### 3. Custom Fields (Property System) Endpoints ### 3. Custom Fields (CustomFieldValue System) Endpoints
#### LiveView Endpoints #### LiveView Endpoints
| Mount | Purpose | Auth | Events | | Mount | Purpose | Auth | Events |
|-------|---------|------|--------| |-------|---------|------|--------|
| `/property-types` | List property types | 🛡️ | `new`, `edit`, `delete` | | `/custom-fields` | List custom fields | 🛡️ | `new`, `edit`, `delete` |
| `/property-types/new` | Create property type | 🛡️ | `save`, `cancel` | | `/custom-fields/new` | Create custom field | 🛡️ | `save`, `cancel` |
| `/property-types/:id/edit` | Edit property type | 🛡️ | `save`, `cancel`, `delete` | | `/custom-fields/:id/edit` | Edit custom field | 🛡️ | `save`, `cancel`, `delete` |
#### Ash Resource Actions #### Ash Resource Actions
| Resource | Action | Purpose | Auth | Input | Output | | Resource | Action | Purpose | Auth | Input | Output |
|----------|--------|---------|------|-------|--------| |----------|--------|---------|------|-------|--------|
| `PropertyType` | `:create` | Create property type | 🛡️ | `{name, value_type, description, ...}` | `{:ok, property_type}` | | `CustomField` | `:create` | Create custom field | 🛡️ | `{name, value_type, description, ...}` | `{:ok, custom_field}` |
| `PropertyType` | `:read` | List property types | 🔐 | - | `[%PropertyType{}]` | | `CustomField` | `:read` | List custom fields | 🔐 | - | `[%CustomField{}]` |
| `PropertyType` | `:update` | Update property type | 🛡️ | `{id, attrs}` | `{:ok, property_type}` | | `CustomField` | `:update` | Update custom field | 🛡️ | `{id, attrs}` | `{:ok, custom_field}` |
| `PropertyType` | `:destroy` | Delete property type | 🛡️ | `{id}` | `{:ok, property_type}` | | `CustomField` | `:destroy` | Delete custom field | 🛡️ | `{id}` | `{:ok, custom_field}` |
| `Property` | `:create` | Add property to member | 🔐 | `{member_id, property_type_id, value}` | `{:ok, property}` | | `CustomFieldValue` | `:create` | Add custom field value to member | 🔐 | `{member_id, custom_field_id, value}` | `{:ok, custom_field_value}` |
| `Property` | `:update` | Update property value | 🔐 | `{id, value}` | `{:ok, property}` | | `CustomFieldValue` | `:update` | Update custom field value | 🔐 | `{id, value}` | `{:ok, custom_field_value}` |
| `Property` | `:destroy` | Remove property | 🔐 | `{id}` | `{:ok, property}` | | `CustomFieldValue` | `:destroy` | Remove custom field value | 🔐 | `{id}` | `{:ok, custom_field_value}` |
#### **NEW: Enhanced Custom Fields** (Issue #194, #157, #161, #153) #### **NEW: Enhanced Custom Fields** (Issue #194, #157, #161, #153)
| Resource | Action | Purpose | Auth | Input | Output | | Resource | Action | Purpose | Auth | Input | Output |
|----------|--------|---------|------|-------|--------| |----------|--------|---------|------|-------|--------|
| `PropertyType` | `:set_default_visibility` | Show/hide by default | 🛡️ | `{id, visible}` | `{:ok, property_type}` | | `CustomField` | `:set_default_visibility` | Show/hide by default | 🛡️ | `{id, visible}` | `{:ok, custom_field}` |
| `PropertyType` | `:set_required` | Mark as required | 🛡️ | `{id, required}` | `{:ok, property_type}` | | `CustomField` | `:set_required` | Mark as required | 🛡️ | `{id, required}` | `{:ok, custom_field}` |
| `PropertyType` | `:add_validation` | Add validation rule | 🛡️ | `{id, rule_type, params}` | `{:ok, property_type}` | | `CustomField` | `:add_validation` | Add validation rule | 🛡️ | `{id, rule_type, params}` | `{:ok, custom_field}` |
| `PropertyType` | `:create_group` | Create field group | 🛡️ | `{name, property_type_ids}` | `{:ok, group}` | | `CustomField` | `:create_group` | Create field group | 🛡️ | `{name, custom_field_ids}` | `{:ok, group}` |
| `Property` | `:validate_value` | Validate property value | 🔐 | `{property_type_id, value}` | `{:ok, valid}` or `{:error, reason}` | | `CustomFieldValue` | `:validate_value` | Validate custom field value | 🔐 | `{custom_field_id, value}` | `{:ok, valid}` or `{:error, reason}` |
--- ---

View file

@ -0,0 +1,101 @@
defmodule Mv.Membership.CustomField do
@moduledoc """
Ash resource defining the schema for custom member fields.
## Overview
CustomFields define the "schema" for custom fields in the membership system.
Each CustomField specifies the name, data type, and behavior of a custom field
that can be attached to members via CustomFieldValue resources.
## Attributes
- `name` - Unique identifier for the custom field (e.g., "phone_mobile", "birthday")
- `value_type` - Data type constraint (`:string`, `:integer`, `:boolean`, `:date`, `:email`)
- `description` - Optional human-readable description
- `immutable` - If true, custom field values cannot be changed after creation
- `required` - If true, all members must have this custom field (future feature)
## Supported Value Types
- `:string` - Text data (max 10,000 characters)
- `:integer` - Numeric data (64-bit integers)
- `:boolean` - True/false flags
- `:date` - Date values (no time component)
- `:email` - Validated email addresses (max 254 characters)
## Relationships
- `has_many :custom_field_values` - All custom field values of this type
## Constraints
- Name must be unique across all custom fields
- Name maximum length: 100 characters
- Cannot delete a custom field that has existing custom field values (RESTRICT)
## Examples
# Create a new custom field
CustomField.create!(%{
name: "phone_mobile",
value_type: :string,
description: "Mobile phone number"
})
# Create a required custom field
CustomField.create!(%{
name: "emergency_contact",
value_type: :string,
required: true
})
"""
use Ash.Resource,
domain: Mv.Membership,
data_layer: AshPostgres.DataLayer
postgres do
table "custom_fields"
repo Mv.Repo
end
actions do
defaults [:create, :read, :update, :destroy]
default_accept [:name, :value_type, :description, :immutable, :required]
end
attributes do
uuid_primary_key :id
attribute :name, :string,
allow_nil?: false,
public?: true,
constraints: [
max_length: 100,
trim?: true
]
attribute :value_type, :atom,
constraints: [one_of: [:string, :integer, :boolean, :date, :email]],
allow_nil?: false,
description: "Defines the datatype `CustomFieldValue.value` is interpreted as"
attribute :description, :string,
allow_nil?: true,
public?: true,
constraints: [
max_length: 500,
trim?: true
]
attribute :immutable, :boolean,
default: false,
allow_nil?: false
attribute :required, :boolean,
default: false,
allow_nil?: false
end
relationships do
has_many :custom_field_values, Mv.Membership.CustomFieldValue
end
identities do
identity :unique_name, [:name]
end
end

View file

@ -0,0 +1,102 @@
defmodule Mv.Membership.CustomFieldValue do
@moduledoc """
Ash resource representing a custom field value for a member.
## Overview
CustomFieldValues implement the Entity-Attribute-Value (EAV) pattern, allowing
dynamic custom fields to be attached to members. Each custom field value links a
member to a custom field and stores the actual value.
## Value Storage
Values are stored using Ash's union type with JSONB storage format:
```json
{
"type": "string",
"value": "example"
}
```
## Supported Types
- `:string` - Text data
- `:integer` - Numeric data
- `:boolean` - True/false flags
- `:date` - Date values
- `:email` - Validated email addresses (custom type)
## Relationships
- `belongs_to :member` - The member this custom field value belongs to (CASCADE delete)
- `belongs_to :custom_field` - The custom field definition
## Constraints
- Each member can have only one custom field value per custom field (unique composite index)
- Custom field values are deleted when the associated member is deleted (CASCADE)
- String values maximum length: 10,000 characters
- Email values maximum length: 254 characters (RFC 5321)
## Future Features
- Type-matching validation (value type must match custom field's value_type) - to be implemented
"""
use Ash.Resource,
domain: Mv.Membership,
data_layer: AshPostgres.DataLayer
postgres do
table "custom_field_values"
repo Mv.Repo
references do
reference :member, on_delete: :delete
end
end
actions do
defaults [:create, :read, :update, :destroy]
default_accept [:value, :member_id, :custom_field_id]
end
attributes do
uuid_primary_key :id
attribute :value, :union,
constraints: [
storage: :type_and_value,
types: [
boolean: [
type: :boolean
],
date: [
type: :date
],
integer: [
type: :integer
],
string: [
type: :string,
constraints: [
max_length: 10_000,
trim?: true
]
],
email: [
type: Mv.Membership.Email
]
]
]
end
relationships do
belongs_to :member, Mv.Membership.Member
belongs_to :custom_field, Mv.Membership.CustomField
end
calculations do
calculate :value_to_string, :string, expr(value[:value] <> "")
end
# Ensure a member can only have one custom field value per custom field
# For example: A member can have only one "phone" custom field value, one "email" custom field value, etc.
identities do
identity :unique_custom_field_per_member, [:member_id, :custom_field_id]
end
end

View file

@ -4,22 +4,23 @@ defmodule Mv.Membership.Email do
## Overview ## Overview
This type extends `:string` with email-specific validation constraints. This type extends `:string` with email-specific validation constraints.
It ensures that email values stored in Property resources are valid email It ensures that email values stored in CustomFieldValue resources are valid email
addresses according to a standard regex pattern. addresses according to a standard regex pattern.
## Validation Rules ## Validation Rules
- Minimum length: 5 characters - **Optional**: `nil` and empty strings are allowed (custom fields are optional)
- Minimum length: 5 characters (for non-empty values)
- Maximum length: 254 characters (RFC 5321 maximum) - Maximum length: 254 characters (RFC 5321 maximum)
- Pattern: Standard email format (username@domain.tld) - Pattern: Standard email format (username@domain.tld)
- Automatic trimming of leading/trailing whitespace - Automatic trimming of leading/trailing whitespace (empty strings become `nil`)
## Usage ## Usage
This type is used in the Property union type for properties with This type is used in the CustomFieldValue union type for custom fields with
`value_type: :email` in PropertyType definitions. `value_type: :email` in CustomField definitions.
## Example ## Example
# In a property type definition # In a custom field definition
PropertyType.create!(%{ CustomField.create!(%{
name: "work_email", name: "work_email",
value_type: :email value_type: :email
}) })
@ -46,11 +47,18 @@ defmodule Mv.Membership.Email do
max_length: @max_length max_length: @max_length
] ]
@impl true
def cast_input(nil, _), do: {:ok, nil}
@impl true @impl true
def cast_input(value, _) when is_binary(value) do def cast_input(value, _) when is_binary(value) do
value = String.trim(value) value = String.trim(value)
cond do cond do
# Empty string after trim becomes nil (optional field)
value == "" ->
{:ok, nil}
String.length(value) < @min_length -> String.length(value) < @min_length ->
:error :error

View file

@ -7,7 +7,7 @@ defmodule Mv.Membership.Member do
can have: can have:
- Personal information (name, email, phone, address) - Personal information (name, email, phone, address)
- Optional link to a User account (1:1 relationship) - Optional link to a User account (1:1 relationship)
- Dynamic custom properties via PropertyType system - Dynamic custom field values via CustomField system
- Full-text searchable profile - Full-text searchable profile
## Email Synchronization ## Email Synchronization
@ -16,7 +16,7 @@ defmodule Mv.Membership.Member do
See `Mv.EmailSync` for details. See `Mv.EmailSync` for details.
## Relationships ## Relationships
- `has_many :properties` - Dynamic custom fields - `has_many :custom_field_values` - Dynamic custom fields
- `has_one :user` - Optional authentication account link - `has_one :user` - Optional authentication account link
## Validations ## Validations
@ -48,8 +48,8 @@ defmodule Mv.Membership.Member do
create :create_member do create :create_member do
primary? true primary? true
# Properties can be created along with member # Custom field values can be created along with member
argument :properties, {:array, :map} argument :custom_field_values, {:array, :map}
# Allow user to be passed as argument for relationship management # Allow user to be passed as argument for relationship management
# user_id is NOT in accept list to prevent direct foreign key manipulation # user_id is NOT in accept list to prevent direct foreign key manipulation
argument :user, :map, allow_nil?: true argument :user, :map, allow_nil?: true
@ -70,7 +70,7 @@ defmodule Mv.Membership.Member do
:postal_code :postal_code
] ]
change manage_relationship(:properties, type: :create) change manage_relationship(:custom_field_values, type: :create)
# Manage the user relationship during member creation # Manage the user relationship during member creation
change manage_relationship(:user, :user, change manage_relationship(:user, :user,
@ -95,8 +95,8 @@ defmodule Mv.Membership.Member do
primary? true primary? true
# Required because custom validation function cannot be done atomically # Required because custom validation function cannot be done atomically
require_atomic? false require_atomic? false
# Properties can be updated or created along with member # Custom field values can be updated or created along with member
argument :properties, {:array, :map} argument :custom_field_values, {:array, :map}
# Allow user to be passed as argument for relationship management # Allow user to be passed as argument for relationship management
# user_id is NOT in accept list to prevent direct foreign key manipulation # user_id is NOT in accept list to prevent direct foreign key manipulation
argument :user, :map, allow_nil?: true argument :user, :map, allow_nil?: true
@ -117,7 +117,7 @@ defmodule Mv.Membership.Member do
:postal_code :postal_code
] ]
change manage_relationship(:properties, on_match: :update, on_no_match: :create) change manage_relationship(:custom_field_values, on_match: :update, on_no_match: :create)
# Manage the user relationship during member update # Manage the user relationship during member update
change manage_relationship(:user, :user, change manage_relationship(:user, :user,
@ -349,7 +349,7 @@ defmodule Mv.Membership.Member do
end end
relationships do relationships do
has_many :properties, Mv.Membership.Property has_many :custom_field_values, Mv.Membership.CustomFieldValue
# 1:1 relationship - Member can optionally have one User # 1:1 relationship - Member can optionally have one User
# This references the User's member_id attribute # This references the User's member_id attribute
# The relationship is optional (allow_nil? true by default) # The relationship is optional (allow_nil? true by default)

View file

@ -3,15 +3,15 @@ defmodule Mv.Membership do
Ash Domain for membership management. Ash Domain for membership management.
## Resources ## Resources
- `Member` - Club members with personal information and custom properties - `Member` - Club members with personal information and custom field values
- `Property` - Dynamic custom field values attached to members - `CustomFieldValue` - Dynamic custom field values attached to members
- `PropertyType` - Schema definitions for custom properties - `CustomField` - Schema definitions for custom fields
## Public API ## Public API
The domain exposes these main actions: The domain exposes these main actions:
- Member CRUD: `create_member/1`, `list_members/0`, `update_member/2`, `destroy_member/1` - Member CRUD: `create_member/1`, `list_members/0`, `update_member/2`, `destroy_member/1`
- Property management: `create_property/1`, `list_property/0`, etc. - Custom field value management: `create_custom_field_value/1`, `list_custom_field_values/0`, etc.
- PropertyType management: `create_property_type/1`, `list_property_types/0`, etc. - Custom field management: `create_custom_field/1`, `list_custom_fields/0`, etc.
## Admin Interface ## Admin Interface
The domain is configured with AshAdmin for management UI. The domain is configured with AshAdmin for management UI.
@ -31,18 +31,18 @@ defmodule Mv.Membership do
define :destroy_member, action: :destroy define :destroy_member, action: :destroy
end end
resource Mv.Membership.Property do resource Mv.Membership.CustomFieldValue do
define :create_property, action: :create define :create_custom_field_value, action: :create
define :list_property, action: :read define :list_custom_field_values, action: :read
define :update_property, action: :update define :update_custom_field_value, action: :update
define :destroy_property, action: :destroy define :destroy_custom_field_value, action: :destroy
end end
resource Mv.Membership.PropertyType do resource Mv.Membership.CustomField do
define :create_property_type, action: :create define :create_custom_field, action: :create
define :list_property_types, action: :read define :list_custom_fields, action: :read
define :update_property_type, action: :update define :update_custom_field, action: :update
define :destroy_property_type, action: :destroy define :destroy_custom_field, action: :destroy
end end
end end
end end

View file

@ -1,83 +0,0 @@
defmodule Mv.Membership.Property do
@moduledoc """
Ash resource representing a custom property value for a member.
## Overview
Properties implement the Entity-Attribute-Value (EAV) pattern, allowing
dynamic custom fields to be attached to members. Each property links a
member to a property type and stores the actual value.
## Value Storage
Values are stored using Ash's union type with JSONB storage format:
```json
{
"type": "string",
"value": "example"
}
```
## Supported Types
- `:string` - Text data
- `:integer` - Numeric data
- `:boolean` - True/false flags
- `:date` - Date values
- `:email` - Validated email addresses (custom type)
## Relationships
- `belongs_to :member` - The member this property belongs to (CASCADE delete)
- `belongs_to :property_type` - The property type definition
## Constraints
- Each member can have only one property per property type (unique composite index)
- Properties are deleted when the associated member is deleted (CASCADE)
"""
use Ash.Resource,
domain: Mv.Membership,
data_layer: AshPostgres.DataLayer
postgres do
table "properties"
repo Mv.Repo
references do
reference :member, on_delete: :delete
end
end
actions do
defaults [:create, :read, :update, :destroy]
default_accept [:value, :member_id, :property_type_id]
end
attributes do
uuid_primary_key :id
attribute :value, :union,
constraints: [
storage: :type_and_value,
types: [
boolean: [type: :boolean],
date: [type: :date],
integer: [type: :integer],
string: [type: :string],
email: [type: Mv.Membership.Email]
]
]
end
relationships do
belongs_to :member, Mv.Membership.Member
belongs_to :property_type, Mv.Membership.PropertyType
end
calculations do
calculate :value_to_string, :string, expr(value[:value] <> "")
end
# Ensure a member can only have one property per property type
# For example: A member can have only one "email" property, one "phone" property, etc.
identities do
identity :unique_property_per_member, [:member_id, :property_type_id]
end
end

View file

@ -1,88 +0,0 @@
defmodule Mv.Membership.PropertyType do
@moduledoc """
Ash resource defining the schema for custom member properties.
## Overview
PropertyTypes define the "schema" for custom fields in the membership system.
Each PropertyType specifies the name, data type, and behavior of a custom field
that can be attached to members via Property resources.
## Attributes
- `name` - Unique identifier for the property (e.g., "phone_mobile", "birthday")
- `value_type` - Data type constraint (`:string`, `:integer`, `:boolean`, `:date`, `:email`)
- `description` - Optional human-readable description
- `immutable` - If true, property values cannot be changed after creation
- `required` - If true, all members must have this property (future feature)
## Supported Value Types
- `:string` - Text data (unlimited length)
- `:integer` - Numeric data (64-bit integers)
- `:boolean` - True/false flags
- `:date` - Date values (no time component)
- `:email` - Validated email addresses
## Relationships
- `has_many :properties` - All property values of this type
## Constraints
- Name must be unique across all property types
- Cannot delete a property type that has existing property values (RESTRICT)
## Examples
# Create a new property type
PropertyType.create!(%{
name: "phone_mobile",
value_type: :string,
description: "Mobile phone number"
})
# Create a required property type
PropertyType.create!(%{
name: "emergency_contact",
value_type: :string,
required: true
})
"""
use Ash.Resource,
domain: Mv.Membership,
data_layer: AshPostgres.DataLayer
postgres do
table "property_types"
repo Mv.Repo
end
actions do
defaults [:create, :read, :update, :destroy]
default_accept [:name, :value_type, :description, :immutable, :required]
end
attributes do
uuid_primary_key :id
attribute :name, :string, allow_nil?: false, public?: true
attribute :value_type, :atom,
constraints: [one_of: [:string, :integer, :boolean, :date, :email]],
allow_nil?: false,
description: "Defines the datatype `Property.value` is interpreted as"
attribute :description, :string, allow_nil?: true, public?: true
attribute :immutable, :boolean,
default: false,
allow_nil?: false
attribute :required, :boolean,
default: false,
allow_nil?: false
end
relationships do
has_many :properties, Mv.Membership.Property
end
identities do
identity :unique_name, [:name]
end
end

View file

@ -17,6 +17,7 @@ defmodule MvWeb.Layouts.Navbar do
<a class="btn btn-ghost text-xl">Mitgliederverwaltung</a> <a class="btn btn-ghost text-xl">Mitgliederverwaltung</a>
<ul class="menu menu-horizontal bg-base-200"> <ul class="menu menu-horizontal bg-base-200">
<li><.link navigate="/members">{gettext("Members")}</.link></li> <li><.link navigate="/members">{gettext("Members")}</.link></li>
<li><.link navigate="/custom_fields">{gettext("Custom Fields")}</.link></li>
<li><.link navigate="/users">{gettext("Users")}</.link></li> <li><.link navigate="/users">{gettext("Users")}</.link></li>
</ul> </ul>
</div> </div>

View file

@ -1,10 +1,10 @@
defmodule MvWeb.PropertyTypeLive.Form do defmodule MvWeb.CustomFieldLive.Form do
@moduledoc """ @moduledoc """
LiveView form for creating and editing property types (admin). LiveView form for creating and editing custom fields (admin).
## Features ## Features
- Create new property type definitions - Create new custom field definitions
- Edit existing property types - Edit existing custom fields
- Select value type from supported types - Select value type from supported types
- Set immutable and required flags - Set immutable and required flags
- Real-time validation - Real-time validation
@ -17,7 +17,7 @@ defmodule MvWeb.PropertyTypeLive.Form do
**Optional:** **Optional:**
- description - Human-readable explanation - description - Human-readable explanation
- immutable - If true, values cannot be changed after creation (default: false) - immutable - If true, values cannot be changed after creation (default: false)
- required - If true, all members must have this property (default: false) - required - If true, all members must have this custom field (default: false)
## Value Type Selection ## Value Type Selection
- `:string` - Text data (unlimited length) - `:string` - Text data (unlimited length)
@ -28,10 +28,10 @@ defmodule MvWeb.PropertyTypeLive.Form do
## Events ## Events
- `validate` - Real-time form validation - `validate` - Real-time form validation
- `save` - Submit form (create or update property type) - `save` - Submit form (create or update custom field)
## Security ## Security
Property type management is restricted to admin users. Custom field management is restricted to admin users.
""" """
use MvWeb, :live_view use MvWeb, :live_view
@ -42,18 +42,18 @@ defmodule MvWeb.PropertyTypeLive.Form do
<.header> <.header>
{@page_title} {@page_title}
<:subtitle> <:subtitle>
{gettext("Use this form to manage property_type records in your database.")} {gettext("Use this form to manage custom_field records in your database.")}
</:subtitle> </:subtitle>
</.header> </.header>
<.form for={@form} id="property_type-form" phx-change="validate" phx-submit="save"> <.form for={@form} id="custom_field-form" phx-change="validate" phx-submit="save">
<.input field={@form[:name]} type="text" label={gettext("Name")} /> <.input field={@form[:name]} type="text" label={gettext("Name")} />
<.input <.input
field={@form[:value_type]} field={@form[:value_type]}
type="select" type="select"
label={gettext("Value type")} label={gettext("Value type")}
options={ options={
Ash.Resource.Info.attribute(Mv.Membership.PropertyType, :value_type).constraints[:one_of] Ash.Resource.Info.attribute(Mv.Membership.CustomField, :value_type).constraints[:one_of]
} }
/> />
<.input field={@form[:description]} type="text" label={gettext("Description")} /> <.input field={@form[:description]} type="text" label={gettext("Description")} />
@ -61,9 +61,9 @@ defmodule MvWeb.PropertyTypeLive.Form do
<.input field={@form[:required]} type="checkbox" label={gettext("Required")} /> <.input field={@form[:required]} type="checkbox" label={gettext("Required")} />
<.button phx-disable-with={gettext("Saving...")} variant="primary"> <.button phx-disable-with={gettext("Saving...")} variant="primary">
{gettext("Save Property type")} {gettext("Save Custom field")}
</.button> </.button>
<.button navigate={return_path(@return_to, @property_type)}>{gettext("Cancel")}</.button> <.button navigate={return_path(@return_to, @custom_field)}>{gettext("Cancel")}</.button>
</.form> </.form>
</Layouts.app> </Layouts.app>
""" """
@ -71,19 +71,19 @@ defmodule MvWeb.PropertyTypeLive.Form do
@impl true @impl true
def mount(params, _session, socket) do def mount(params, _session, socket) do
property_type = custom_field =
case params["id"] do case params["id"] do
nil -> nil nil -> nil
id -> Ash.get!(Mv.Membership.PropertyType, id) id -> Ash.get!(Mv.Membership.CustomField, id)
end end
action = if is_nil(property_type), do: "New", else: "Edit" action = if is_nil(custom_field), do: "New", else: "Edit"
page_title = action <> " " <> "Property type" page_title = action <> " " <> "Custom field"
{:ok, {:ok,
socket socket
|> assign(:return_to, return_to(params["return_to"])) |> assign(:return_to, return_to(params["return_to"]))
|> assign(property_type: property_type) |> assign(custom_field: custom_field)
|> assign(:page_title, page_title) |> assign(:page_title, page_title)
|> assign_form()} |> assign_form()}
end end
@ -92,15 +92,15 @@ defmodule MvWeb.PropertyTypeLive.Form do
defp return_to(_), do: "index" defp return_to(_), do: "index"
@impl true @impl true
def handle_event("validate", %{"property_type" => property_type_params}, socket) do def handle_event("validate", %{"custom_field" => custom_field_params}, socket) do
{:noreply, {:noreply,
assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, property_type_params))} assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, custom_field_params))}
end end
def handle_event("save", %{"property_type" => property_type_params}, socket) do def handle_event("save", %{"custom_field" => custom_field_params}, socket) do
case AshPhoenix.Form.submit(socket.assigns.form, params: property_type_params) do case AshPhoenix.Form.submit(socket.assigns.form, params: custom_field_params) do
{:ok, property_type} -> {:ok, custom_field} ->
notify_parent({:saved, property_type}) notify_parent({:saved, custom_field})
action = action =
case socket.assigns.form.source.type do case socket.assigns.form.source.type do
@ -111,8 +111,8 @@ defmodule MvWeb.PropertyTypeLive.Form do
socket = socket =
socket socket
|> put_flash(:info, gettext("Property type %{action} successfully", action: action)) |> put_flash(:info, gettext("Custom field %{action} successfully", action: action))
|> push_navigate(to: return_path(socket.assigns.return_to, property_type)) |> push_navigate(to: return_path(socket.assigns.return_to, custom_field))
{:noreply, socket} {:noreply, socket}
@ -123,17 +123,17 @@ defmodule MvWeb.PropertyTypeLive.Form do
defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
defp assign_form(%{assigns: %{property_type: property_type}} = socket) do defp assign_form(%{assigns: %{custom_field: custom_field}} = socket) do
form = form =
if property_type do if custom_field do
AshPhoenix.Form.for_update(property_type, :update, as: "property_type") AshPhoenix.Form.for_update(custom_field, :update, as: "custom_field")
else else
AshPhoenix.Form.for_create(Mv.Membership.PropertyType, :create, as: "property_type") AshPhoenix.Form.for_create(Mv.Membership.CustomField, :create, as: "custom_field")
end end
assign(socket, form: to_form(form)) assign(socket, form: to_form(form))
end end
defp return_path("index", _property_type), do: ~p"/property_types" defp return_path("index", _custom_field), do: ~p"/custom_fields"
defp return_path("show", property_type), do: ~p"/property_types/#{property_type.id}" defp return_path("show", custom_field), do: ~p"/custom_fields/#{custom_field.id}"
end end

View file

@ -0,0 +1,88 @@
defmodule MvWeb.CustomFieldLive.Index do
@moduledoc """
LiveView for managing custom field definitions (admin).
## Features
- List all custom fields
- Display type information (name, value type, description)
- Show immutable and required flags
- Create new custom fields
- Edit existing custom fields
- Delete custom fields (if no custom field values use them)
## Displayed Information
- Name: Unique identifier for the custom field
- Value type: Data type constraint (string, integer, boolean, date, email)
- Description: Human-readable explanation
- Immutable: Whether custom field values can be changed after creation
- Required: Whether all members must have this custom field (future feature)
## Events
- `delete` - Remove a custom field (only if no custom field values exist)
## Security
Custom field management is restricted to admin users.
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Listing Custom fields
<:actions>
<.button variant="primary" navigate={~p"/custom_fields/new"}>
<.icon name="hero-plus" /> New Custom field
</.button>
</:actions>
</.header>
<.table
id="custom_fields"
rows={@streams.custom_fields}
row_click={fn {_id, custom_field} -> JS.navigate(~p"/custom_fields/#{custom_field}") end}
>
<:col :let={{_id, custom_field}} label="Id">{custom_field.id}</:col>
<:col :let={{_id, custom_field}} label="Name">{custom_field.name}</:col>
<:col :let={{_id, custom_field}} label="Description">{custom_field.description}</:col>
<:action :let={{_id, custom_field}}>
<div class="sr-only">
<.link navigate={~p"/custom_fields/#{custom_field}"}>Show</.link>
</div>
<.link navigate={~p"/custom_fields/#{custom_field}/edit"}>Edit</.link>
</:action>
<:action :let={{id, custom_field}}>
<.link
phx-click={JS.push("delete", value: %{id: custom_field.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Listing Custom fields")
|> stream(:custom_fields, Ash.read!(Mv.Membership.CustomField))}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
custom_field = Ash.get!(Mv.Membership.CustomField, id)
Ash.destroy!(custom_field)
{:noreply, stream_delete(socket, :custom_fields, custom_field)}
end
end

View file

@ -0,0 +1,66 @@
defmodule MvWeb.CustomFieldLive.Show do
@moduledoc """
LiveView for displaying a single custom field's details (admin).
## Features
- Display custom field definition
- Show all attributes (name, value type, description, flags)
- Navigate to edit form
- Return to custom field list
## Displayed Information
- Name: Unique identifier
- Value type: Data type constraint
- Description: Optional explanation
- Immutable flag: Whether values can be changed
- Required flag: Whether all members need this custom field
## Navigation
- Back to custom field list
- Edit custom field
## Security
Custom field details are restricted to admin users.
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Custom field {@custom_field.id}
<:subtitle>This is a custom_field record from your database.</:subtitle>
<:actions>
<.button navigate={~p"/custom_fields"}>
<.icon name="hero-arrow-left" />
</.button>
<.button
variant="primary"
navigate={~p"/custom_fields/#{@custom_field}/edit?return_to=show"}
>
<.icon name="hero-pencil-square" /> Edit Custom field
</.button>
</:actions>
</.header>
<.list>
<:item title="Id">{@custom_field.id}</:item>
<:item title="Name">{@custom_field.name}</:item>
<:item title="Description">{@custom_field.description}</:item>
</.list>
</Layouts.app>
"""
end
@impl true
def mount(%{"id" => id}, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Show Custom field")
|> assign(:custom_field, Ash.get!(Mv.Membership.CustomField, id))}
end
end

View file

@ -1,21 +1,21 @@
defmodule MvWeb.PropertyLive.Form do defmodule MvWeb.CustomFieldValueLive.Form do
@moduledoc """ @moduledoc """
LiveView form for creating and editing properties. LiveView form for creating and editing custom field values.
## Features ## Features
- Create new properties with member and type selection - Create new custom field values with member and type selection
- Edit existing property values - Edit existing custom field values
- Value input adapts to property type (string, integer, boolean, date, email) - Value input adapts to custom field type (string, integer, boolean, date, email)
- Real-time validation - Real-time validation
## Form Fields ## Form Fields
**Required:** **Required:**
- member - Select which member owns this property - member - Select which member owns this custom field value
- property_type - Select the type (defines value type) - custom_field - Select the type (defines value type)
- value - The actual value (input type depends on property type) - value - The actual value (input type depends on custom field type)
## Value Types ## Value Types
The form dynamically renders appropriate inputs based on property type: The form dynamically renders appropriate inputs based on custom field type:
- String: text input - String: text input
- Integer: number input - Integer: number input
- Boolean: checkbox - Boolean: checkbox
@ -24,10 +24,10 @@ defmodule MvWeb.PropertyLive.Form do
## Events ## Events
- `validate` - Real-time form validation - `validate` - Real-time form validation
- `save` - Submit form (create or update property) - `save` - Submit form (create or update custom field value)
## Note ## Note
Properties are typically managed through the member edit form, Custom field values are typically managed through the member edit form,
not through this standalone form. not through this standalone form.
""" """
use MvWeb, :live_view use MvWeb, :live_view
@ -38,17 +38,19 @@ defmodule MvWeb.PropertyLive.Form do
<Layouts.app flash={@flash} current_user={@current_user}> <Layouts.app flash={@flash} current_user={@current_user}>
<.header> <.header>
{@page_title} {@page_title}
<:subtitle>{gettext("Use this form to manage property records in your database.")}</:subtitle> <:subtitle>
{gettext("Use this form to manage custom_field_value records in your database.")}
moritz marked this conversation as resolved

Maybe without underscore?

Maybe without underscore?
</:subtitle>
</.header> </.header>
<.form for={@form} id="property-form" phx-change="validate" phx-submit="save"> <.form for={@form} id="custom_field_value-form" phx-change="validate" phx-submit="save">
<!-- Property Type Selection --> <!-- Custom Field Selection -->
<.input <.input
field={@form[:property_type_id]} field={@form[:custom_field_id]}
type="select" type="select"
label={gettext("Property type")} label={gettext("Custom field")}
options={property_type_options(@property_types)} options={custom_field_options(@custom_fields)}
prompt={gettext("Choose a property type")} prompt={gettext("Choose a custom field")}
/> />
<!-- Member Selection --> <!-- Member Selection -->
@ -61,18 +63,18 @@ defmodule MvWeb.PropertyLive.Form do
/> />
<!-- Value Input - handles Union type --> <!-- Value Input - handles Union type -->
<%= if @selected_property_type do %> <%= if @selected_custom_field do %>
<.union_value_input form={@form} property_type={@selected_property_type} /> <.union_value_input form={@form} custom_field={@selected_custom_field} />
<% else %> <% else %>
<div class="text-sm text-gray-600"> <div class="text-sm text-gray-600">
{gettext("Please select a property type first")} {gettext("Please select a custom field first")}
</div> </div>
<% end %> <% end %>
<.button phx-disable-with={gettext("Saving...")} variant="primary"> <.button phx-disable-with={gettext("Saving...")} variant="primary">
{gettext("Save Property")} {gettext("Save Custom field value")}
</.button> </.button>
<.button navigate={return_path(@return_to, @property)}>{gettext("Cancel")}</.button> <.button navigate={return_path(@return_to, @custom_field_value)}>{gettext("Cancel")}</.button>
</.form> </.form>
</Layouts.app> </Layouts.app>
""" """
@ -80,8 +82,8 @@ defmodule MvWeb.PropertyLive.Form do
# Helper function for Union-Value Input # Helper function for Union-Value Input
defp union_value_input(assigns) do defp union_value_input(assigns) do
# Extract the current value from the Property # Extract the current value from the CustomFieldValue
current_value = extract_current_value(assigns.form.data, assigns.property_type.value_type) current_value = extract_current_value(assigns.form.data, assigns.custom_field.value_type)
assigns = assign(assigns, :current_value, current_value) assigns = assign(assigns, :current_value, current_value)
~H""" ~H"""
@ -90,7 +92,7 @@ defmodule MvWeb.PropertyLive.Form do
{gettext("Value")} {gettext("Value")}
</label> </label>
<%= case @property_type.value_type do %> <%= case @custom_field.value_type do %>
<% :string -> %> <% :string -> %>
<.inputs_for :let={value_form} field={@form[:value]}> <.inputs_for :let={value_form} field={@form[:value]}>
<.input field={value_form[:value]} type="text" label="" value={@current_value} /> <.input field={value_form[:value]} type="text" label="" value={@current_value} />
@ -123,16 +125,16 @@ defmodule MvWeb.PropertyLive.Form do
</.inputs_for> </.inputs_for>
<% _ -> %> <% _ -> %>
<div class="text-sm text-red-600"> <div class="text-sm text-red-600">
{gettext("Unsupported value type: %{type}", type: @property_type.value_type)} {gettext("Unsupported value type: %{type}", type: @custom_field.value_type)}
</div> </div>
<% end %> <% end %>
</div> </div>
""" """
end end
# Helper function to extract the current value from the Property # Helper function to extract the current value from the CustomFieldValue
defp extract_current_value( defp extract_current_value(
%Mv.Membership.Property{value: %Ash.Union{value: value}}, %Mv.Membership.CustomFieldValue{value: %Ash.Union{value: value}},
_value_type _value_type
) do ) do
value value
@ -160,27 +162,27 @@ defmodule MvWeb.PropertyLive.Form do
@impl true @impl true
def mount(params, _session, socket) do def mount(params, _session, socket) do
property = custom_field_value =
case params["id"] do case params["id"] do
nil -> nil nil -> nil
id -> Ash.get!(Mv.Membership.Property, id) |> Ash.load!([:property_type]) id -> Ash.get!(Mv.Membership.CustomFieldValue, id) |> Ash.load!([:custom_field])
end end
action = if is_nil(property), do: "New", else: "Edit" action = if is_nil(custom_field_value), do: "New", else: "Edit"
page_title = action <> " " <> "Property" page_title = action <> " " <> "Custom field value"
# Load all PropertyTypes and Members for the selection fields # Load all CustomFields and Members for the selection fields
property_types = Ash.read!(Mv.Membership.PropertyType) custom_fields = Ash.read!(Mv.Membership.CustomField)
members = Ash.read!(Mv.Membership.Member) members = Ash.read!(Mv.Membership.Member)
{:ok, {:ok,
socket socket
|> assign(:return_to, return_to(params["return_to"])) |> assign(:return_to, return_to(params["return_to"]))
|> assign(property: property) |> assign(custom_field_value: custom_field_value)
|> assign(:page_title, page_title) |> assign(:page_title, page_title)
|> assign(:property_types, property_types) |> assign(:custom_fields, custom_fields)
|> assign(:members, members) |> assign(:members, members)
|> assign(:selected_property_type, property && property.property_type) |> assign(:selected_custom_field, custom_field_value && custom_field_value.custom_field)
|> assign_form()} |> assign_form()}
end end
@ -188,43 +190,43 @@ defmodule MvWeb.PropertyLive.Form do
defp return_to(_), do: "index" defp return_to(_), do: "index"
@impl true @impl true
def handle_event("validate", %{"property" => property_params}, socket) do def handle_event("validate", %{"custom_field_value" => custom_field_value_params}, socket) do
# Find the selected PropertyType # Find the selected CustomField
selected_property_type = selected_custom_field =
case property_params["property_type_id"] do case custom_field_value_params["custom_field_id"] do
"" -> nil "" -> nil
nil -> nil nil -> nil
id -> Enum.find(socket.assigns.property_types, &(&1.id == id)) id -> Enum.find(socket.assigns.custom_fields, &(&1.id == id))
end end
# Set the Union type based on the selected PropertyType # Set the Union type based on the selected CustomField
updated_params = updated_params =
if selected_property_type do if selected_custom_field do
union_type = to_string(selected_property_type.value_type) union_type = to_string(selected_custom_field.value_type)
put_in(property_params, ["value", "_union_type"], union_type) put_in(custom_field_value_params, ["value", "_union_type"], union_type)
else else
property_params custom_field_value_params
end end
{:noreply, {:noreply,
socket socket
|> assign(:selected_property_type, selected_property_type) |> assign(:selected_custom_field, selected_custom_field)
|> assign(form: AshPhoenix.Form.validate(socket.assigns.form, updated_params))} |> assign(form: AshPhoenix.Form.validate(socket.assigns.form, updated_params))}
end end
def handle_event("save", %{"property" => property_params}, socket) do def handle_event("save", %{"custom_field_value" => custom_field_value_params}, socket) do
# Set the Union type based on the selected PropertyType # Set the Union type based on the selected CustomField
updated_params = updated_params =
if socket.assigns.selected_property_type do if socket.assigns.selected_custom_field do
union_type = to_string(socket.assigns.selected_property_type.value_type) union_type = to_string(socket.assigns.selected_custom_field.value_type)
put_in(property_params, ["value", "_union_type"], union_type) put_in(custom_field_value_params, ["value", "_union_type"], union_type)
else else
property_params custom_field_value_params
end end
case AshPhoenix.Form.submit(socket.assigns.form, params: updated_params) do case AshPhoenix.Form.submit(socket.assigns.form, params: updated_params) do
{:ok, property} -> {:ok, custom_field_value} ->
notify_parent({:saved, property}) notify_parent({:saved, custom_field_value})
action = action =
case socket.assigns.form.source.type do case socket.assigns.form.source.type do
@ -235,8 +237,11 @@ defmodule MvWeb.PropertyLive.Form do
socket = socket =
socket socket
|> put_flash(:info, gettext("Property %{action} successfully", action: action)) |> put_flash(
|> push_navigate(to: return_path(socket.assigns.return_to, property)) :info,
gettext("Custom field value %{action} successfully", action: action)
)
|> push_navigate(to: return_path(socket.assigns.return_to, custom_field_value))
{:noreply, socket} {:noreply, socket}
@ -247,11 +252,11 @@ defmodule MvWeb.PropertyLive.Form do
defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
defp assign_form(%{assigns: %{property: property}} = socket) do defp assign_form(%{assigns: %{custom_field_value: custom_field_value}} = socket) do
form = form =
if property do if custom_field_value do
# Determine the Union type based on the property_type # Determine the Union type based on the custom_field
union_type = property.property_type && property.property_type.value_type union_type = custom_field_value.custom_field && custom_field_value.custom_field.value_type
params = params =
if union_type do if union_type do
@ -260,20 +265,27 @@ defmodule MvWeb.PropertyLive.Form do
%{} %{}
end end
AshPhoenix.Form.for_update(property, :update, as: "property", params: params) AshPhoenix.Form.for_update(custom_field_value, :update,
as: "custom_field_value",
params: params
)
else else
AshPhoenix.Form.for_create(Mv.Membership.Property, :create, as: "property") AshPhoenix.Form.for_create(Mv.Membership.CustomFieldValue, :create,
as: "custom_field_value"
)
end end
assign(socket, form: to_form(form)) assign(socket, form: to_form(form))
end end
defp return_path("index", _property), do: ~p"/properties" defp return_path("index", _custom_field_value), do: ~p"/custom_field_values"
defp return_path("show", property), do: ~p"/properties/#{property.id}"
defp return_path("show", custom_field_value),
do: ~p"/custom_field_values/#{custom_field_value.id}"
# Helper functions for selection options # Helper functions for selection options
defp property_type_options(property_types) do defp custom_field_options(custom_fields) do
Enum.map(property_types, &{&1.name, &1.id}) Enum.map(custom_fields, &{&1.name, &1.id})
end end
defp member_options(members) do defp member_options(members) do

View file

@ -0,0 +1,86 @@
defmodule MvWeb.CustomFieldValueLive.Index do
@moduledoc """
LiveView for displaying and managing custom field values.
## Features
- List all custom field values with their values and types
- Show which member each custom field value belongs to
- Display custom field information
- Navigate to custom field value details and edit forms
- Delete custom field values
## Relationships
Each custom field value is linked to:
- A member (the custom field value owner)
- A custom field (defining value type and behavior)
## Events
- `delete` - Remove a custom field value from the database
## Note
Custom field values are typically managed through the member edit form.
This view provides a global overview of all custom field values.
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Listing Custom field values
<:actions>
<.button variant="primary" navigate={~p"/custom_field_values/new"}>
<.icon name="hero-plus" /> New Custom field value
</.button>
</:actions>
</.header>
<.table
id="custom_field_values"
rows={@streams.custom_field_values}
row_click={
fn {_id, custom_field_value} ->
JS.navigate(~p"/custom_field_values/#{custom_field_value}")
end
}
>
<:col :let={{_id, custom_field_value}} label="Id">{custom_field_value.id}</:col>
<:action :let={{_id, custom_field_value}}>
<div class="sr-only">
<.link navigate={~p"/custom_field_values/#{custom_field_value}"}>Show</.link>
</div>
<.link navigate={~p"/custom_field_values/#{custom_field_value}/edit"}>Edit</.link>
</:action>
<:action :let={{id, custom_field_value}}>
<.link
phx-click={JS.push("delete", value: %{id: custom_field_value.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Listing Custom field values")
|> stream(:custom_field_values, Ash.read!(Mv.Membership.CustomFieldValue))}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
custom_field_value = Ash.get!(Mv.Membership.CustomFieldValue, id)
Ash.destroy!(custom_field_value)
{:noreply, stream_delete(socket, :custom_field_values, custom_field_value)}
end
end

View file

@ -0,0 +1,67 @@
defmodule MvWeb.CustomFieldValueLive.Show do
@moduledoc """
LiveView for displaying a single custom field value's details.
## Features
- Display custom field value and type
- Show linked member
- Show custom field definition
- Navigate to edit form
- Return to custom field value list
## Displayed Information
- Custom field value (formatted based on type)
- Custom field name and description
- Member information (who owns this custom field value)
- Custom field value metadata (ID, timestamps if added)
## Navigation
- Back to custom field value list
- Edit custom field value
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Custom field value {@custom_field_value.id}
<:subtitle>This is a custom_field_value record from your database.</:subtitle>
<:actions>
<.button navigate={~p"/custom_field_values"}>
<.icon name="hero-arrow-left" />
</.button>
<.button
variant="primary"
navigate={~p"/custom_field_values/#{@custom_field_value}/edit?return_to=show"}
>
<.icon name="hero-pencil-square" /> Edit Custom field value
</.button>
</:actions>
</.header>
<.list>
<:item title="Id">{@custom_field_value.id}</:item>
</.list>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:custom_field_value, Ash.get!(Mv.Membership.CustomFieldValue, id))}
end
defp page_title(:show), do: "Show Custom field value"
defp page_title(:edit), do: "Edit Custom field value"
end

View file

@ -19,14 +19,14 @@ defmodule MvWeb.MemberLive.Form do
- paid status - paid status
- notes - notes
## Custom Properties ## Custom Field Values
Members can have dynamic custom properties defined by PropertyTypes. Members can have dynamic custom field values defined by CustomFields.
The form dynamically renders inputs based on available PropertyTypes. The form dynamically renders inputs based on available CustomFields.
## Events ## Events
- `validate` - Real-time form validation - `validate` - Real-time form validation
- `save` - Submit form (create or update member) - `save` - Submit form (create or update member)
- Property management events for adding/removing custom fields - Custom field value management events for adding/removing custom fields
""" """
use MvWeb, :live_view use MvWeb, :live_view
@ -56,10 +56,11 @@ defmodule MvWeb.MemberLive.Form do
<.input field={@form[:house_number]} label={gettext("House Number")} /> <.input field={@form[:house_number]} label={gettext("House Number")} />
<.input field={@form[:postal_code]} label={gettext("Postal Code")} /> <.input field={@form[:postal_code]} label={gettext("Postal Code")} />
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Properties")}</h3> <h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Field Values")}</h3>
<.inputs_for :let={f_property} field={@form[:properties]}> <.inputs_for :let={f_custom_field_value} field={@form[:custom_field_values]}>
<% type = Enum.find(@property_types, &(&1.id == f_property[:property_type_id].value)) %> <% type =
<.inputs_for :let={value_form} field={f_property[:value]}> Enum.find(@custom_fields, &(&1.id == f_custom_field_value[:custom_field_id].value)) %>
<.inputs_for :let={value_form} field={f_custom_field_value[:value]}>
<% input_type = <% input_type =
cond do cond do
type && type.value_type == :boolean -> "checkbox" type && type.value_type == :boolean -> "checkbox"
@ -70,8 +71,8 @@ defmodule MvWeb.MemberLive.Form do
</.inputs_for> </.inputs_for>
<input <input
type="hidden" type="hidden"
name={f_property[:property_type_id].name} name={f_custom_field_value[:custom_field_id].name}
value={f_property[:property_type_id].value} value={f_custom_field_value[:custom_field_id].value}
/> />
</.inputs_for> </.inputs_for>
@ -86,16 +87,16 @@ defmodule MvWeb.MemberLive.Form do
@impl true @impl true
def mount(params, _session, socket) do def mount(params, _session, socket) do
{:ok, property_types} = Mv.Membership.list_property_types() {:ok, custom_fields} = Mv.Membership.list_custom_fields()
initial_properties = initial_custom_field_values =
Enum.map(property_types, fn pt -> Enum.map(custom_fields, fn cf ->
%{ %{
"property_type_id" => pt.id, "custom_field_id" => cf.id,
"value" => %{ "value" => %{
"type" => pt.value_type, "type" => cf.value_type,
"value" => nil, "value" => nil,
"_union_type" => Atom.to_string(pt.value_type) "_union_type" => Atom.to_string(cf.value_type)
} }
} }
end) end)
@ -112,8 +113,8 @@ defmodule MvWeb.MemberLive.Form do
{:ok, {:ok,
socket socket
|> assign(:return_to, return_to(params["return_to"])) |> assign(:return_to, return_to(params["return_to"]))
|> assign(:property_types, property_types) |> assign(:custom_fields, custom_fields)
|> assign(:initial_properties, initial_properties) |> assign(:initial_custom_field_values, initial_custom_field_values)
|> assign(member: member) |> assign(member: member)
|> assign(:page_title, page_title) |> assign(:page_title, page_title)
|> assign_form()} |> assign_form()}
@ -156,25 +157,25 @@ defmodule MvWeb.MemberLive.Form do
defp assign_form(%{assigns: %{member: member}} = socket) do defp assign_form(%{assigns: %{member: member}} = socket) do
form = form =
if member do if member do
{:ok, member} = Ash.load(member, properties: [:property_type]) {:ok, member} = Ash.load(member, custom_field_values: [:custom_field])
existing_properties = existing_custom_field_values =
member.properties member.custom_field_values
|> Enum.map(& &1.property_type_id) |> Enum.map(& &1.custom_field_id)
is_missing_property = fn i -> is_missing_custom_field_value = fn i ->
not Enum.member?(existing_properties, Map.get(i, "property_type_id")) not Enum.member?(existing_custom_field_values, Map.get(i, "custom_field_id"))
end end
params = %{ params = %{
"properties" => "custom_field_values" =>
Enum.map(member.properties, fn prop -> Enum.map(member.custom_field_values, fn cfv ->
%{ %{
"property_type_id" => prop.property_type_id, "custom_field_id" => cfv.custom_field_id,
"value" => %{ "value" => %{
"_union_type" => Atom.to_string(prop.value.type), "_union_type" => Atom.to_string(cfv.value.type),
"type" => prop.value.type, "type" => cfv.value.type,
"value" => prop.value.value "value" => cfv.value.value
} }
} }
end) end)
@ -190,12 +191,13 @@ defmodule MvWeb.MemberLive.Form do
forms: [auto?: true] forms: [auto?: true]
) )
missing_properties = Enum.filter(socket.assigns[:initial_properties], is_missing_property) missing_custom_field_values =
Enum.filter(socket.assigns[:initial_custom_field_values], is_missing_custom_field_value)
Enum.reduce( Enum.reduce(
missing_properties, missing_custom_field_values,
form, form,
&AshPhoenix.Form.add_form(&2, [:properties], params: &1) &AshPhoenix.Form.add_form(&2, [:custom_field_values], params: &1)
) )
else else
AshPhoenix.Form.for_create( AshPhoenix.Form.for_create(
@ -203,7 +205,7 @@ defmodule MvWeb.MemberLive.Form do
:create_member, :create_member,
api: Mv.Membership, api: Mv.Membership,
as: "member", as: "member",
params: %{"properties" => socket.assigns[:initial_properties]}, params: %{"custom_field_values" => socket.assigns[:initial_custom_field_values]},
forms: [auto?: true] forms: [auto?: true]
) )
end end

View file

@ -5,7 +5,7 @@ defmodule MvWeb.MemberLive.Show do
## Features ## Features
- Display all member information (personal, contact, address) - Display all member information (personal, contact, address)
- Show linked user account (if exists) - Show linked user account (if exists)
- Display custom properties - Display custom field values
- Navigate to edit form - Navigate to edit form
- Return to member list - Return to member list
@ -15,7 +15,7 @@ defmodule MvWeb.MemberLive.Show do
- Address: street, house number, postal code, city - Address: street, house number, postal code, city
- Status: paid flag - Status: paid flag
- Relationships: linked user account - Relationships: linked user account
- Custom: dynamic properties from PropertyTypes - Custom: dynamic custom field values from CustomFields
## Navigation ## Navigation
- Back to member list - Back to member list
@ -75,14 +75,14 @@ defmodule MvWeb.MemberLive.Show do
</:item> </:item>
</.list> </.list>
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Properties")}</h3> <h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Field Values")}</h3>
<.generic_list items={ <.generic_list items={
Enum.map(@member.properties, fn p -> Enum.map(@member.custom_field_values, fn cfv ->
{ {
# name # name
p.property_type && p.property_type.name, cfv.custom_field && cfv.custom_field.name,
# value # value
case p.value do case cfv.value do
%{value: v} -> v %{value: v} -> v
v -> v v -> v
end end
@ -103,7 +103,7 @@ defmodule MvWeb.MemberLive.Show do
query = query =
Mv.Membership.Member Mv.Membership.Member
|> filter(id == ^id) |> filter(id == ^id)
|> load([:user, properties: [:property_type]]) |> load([:user, custom_field_values: [:custom_field]])
member = Ash.read_one!(query) member = Ash.read_one!(query)

View file

@ -1,82 +0,0 @@
defmodule MvWeb.PropertyLive.Index do
@moduledoc """
LiveView for displaying and managing properties.
## Features
- List all properties with their values and types
- Show which member each property belongs to
- Display property type information
- Navigate to property details and edit forms
- Delete properties
## Relationships
Each property is linked to:
- A member (the property owner)
- A property type (defining value type and behavior)
## Events
- `delete` - Remove a property from the database
## Note
Properties are typically managed through the member edit form.
This view provides a global overview of all properties.
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Listing Properties
<:actions>
<.button variant="primary" navigate={~p"/properties/new"}>
<.icon name="hero-plus" /> New Property
</.button>
</:actions>
</.header>
<.table
id="properties"
rows={@streams.properties}
row_click={fn {_id, property} -> JS.navigate(~p"/properties/#{property}") end}
>
<:col :let={{_id, property}} label="Id">{property.id}</:col>
<:action :let={{_id, property}}>
<div class="sr-only">
<.link navigate={~p"/properties/#{property}"}>Show</.link>
</div>
<.link navigate={~p"/properties/#{property}/edit"}>Edit</.link>
</:action>
<:action :let={{id, property}}>
<.link
phx-click={JS.push("delete", value: %{id: property.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Listing Properties")
|> stream(:properties, Ash.read!(Mv.Membership.Property))}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
property = Ash.get!(Mv.Membership.Property, id)
Ash.destroy!(property)
{:noreply, stream_delete(socket, :properties, property)}
end
end

View file

@ -1,64 +0,0 @@
defmodule MvWeb.PropertyLive.Show do
@moduledoc """
LiveView for displaying a single property's details.
## Features
- Display property value and type
- Show linked member
- Show property type definition
- Navigate to edit form
- Return to property list
## Displayed Information
- Property value (formatted based on type)
- Property type name and description
- Member information (who owns this property)
- Property metadata (ID, timestamps if added)
## Navigation
- Back to property list
- Edit property
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Property {@property.id}
<:subtitle>This is a property record from your database.</:subtitle>
<:actions>
<.button navigate={~p"/properties"}>
<.icon name="hero-arrow-left" />
</.button>
<.button variant="primary" navigate={~p"/properties/#{@property}/edit?return_to=show"}>
<.icon name="hero-pencil-square" /> Edit Property
</.button>
</:actions>
</.header>
<.list>
<:item title="Id">{@property.id}</:item>
</.list>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:property, Ash.get!(Mv.Membership.Property, id))}
end
defp page_title(:show), do: "Show Property"
defp page_title(:edit), do: "Edit Property"
end

View file

@ -1,88 +0,0 @@
defmodule MvWeb.PropertyTypeLive.Index do
@moduledoc """
LiveView for managing property type definitions (admin).
## Features
- List all property types
- Display type information (name, value type, description)
- Show immutable and required flags
- Create new property types
- Edit existing property types
- Delete property types (if no properties use them)
## Displayed Information
- Name: Unique identifier for the property type
- Value type: Data type constraint (string, integer, boolean, date, email)
- Description: Human-readable explanation
- Immutable: Whether property values can be changed after creation
- Required: Whether all members must have this property (future feature)
## Events
- `delete` - Remove a property type (only if no properties exist)
## Security
Property type management is restricted to admin users.
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Listing Property types
<:actions>
<.button variant="primary" navigate={~p"/property_types/new"}>
<.icon name="hero-plus" /> New Property type
</.button>
</:actions>
</.header>
<.table
id="property_types"
rows={@streams.property_types}
row_click={fn {_id, property_type} -> JS.navigate(~p"/property_types/#{property_type}") end}
>
<:col :let={{_id, property_type}} label="Id">{property_type.id}</:col>
<:col :let={{_id, property_type}} label="Name">{property_type.name}</:col>
<:col :let={{_id, property_type}} label="Description">{property_type.description}</:col>
<:action :let={{_id, property_type}}>
<div class="sr-only">
<.link navigate={~p"/property_types/#{property_type}"}>Show</.link>
</div>
<.link navigate={~p"/property_types/#{property_type}/edit"}>Edit</.link>
</:action>
<:action :let={{id, property_type}}>
<.link
phx-click={JS.push("delete", value: %{id: property_type.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Listing Property types")
|> stream(:property_types, Ash.read!(Mv.Membership.PropertyType))}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
property_type = Ash.get!(Mv.Membership.PropertyType, id)
Ash.destroy!(property_type)
{:noreply, stream_delete(socket, :property_types, property_type)}
end
end

View file

@ -1,66 +0,0 @@
defmodule MvWeb.PropertyTypeLive.Show do
@moduledoc """
LiveView for displaying a single property type's details (admin).
## Features
- Display property type definition
- Show all attributes (name, value type, description, flags)
- Navigate to edit form
- Return to property type list
## Displayed Information
- Name: Unique identifier
- Value type: Data type constraint
- Description: Optional explanation
- Immutable flag: Whether values can be changed
- Required flag: Whether all members need this property
## Navigation
- Back to property type list
- Edit property type
## Security
Property type details are restricted to admin users.
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Property type {@property_type.id}
<:subtitle>This is a property_type record from your database.</:subtitle>
<:actions>
<.button navigate={~p"/property_types"}>
<.icon name="hero-arrow-left" />
</.button>
<.button
variant="primary"
navigate={~p"/property_types/#{@property_type}/edit?return_to=show"}
>
<.icon name="hero-pencil-square" /> Edit Property type
</.button>
</:actions>
</.header>
<.list>
<:item title="Id">{@property_type.id}</:item>
<:item title="Name">{@property_type.name}</:item>
<:item title="Description">{@property_type.description}</:item>
</.list>
</Layouts.app>
"""
end
@impl true
def mount(%{"id" => id}, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Show Property type")
|> assign(:property_type, Ash.get!(Mv.Membership.PropertyType, id))}
end
end

View file

@ -55,17 +55,17 @@ defmodule MvWeb.Router do
live "/members/:id", MemberLive.Show, :show live "/members/:id", MemberLive.Show, :show
live "/members/:id/show/edit", MemberLive.Show, :edit live "/members/:id/show/edit", MemberLive.Show, :edit
live "/property_types", PropertyTypeLive.Index, :index live "/custom_fields", CustomFieldLive.Index, :index
live "/property_types/new", PropertyTypeLive.Form, :new live "/custom_fields/new", CustomFieldLive.Form, :new
live "/property_types/:id/edit", PropertyTypeLive.Form, :edit live "/custom_fields/:id/edit", CustomFieldLive.Form, :edit
live "/property_types/:id", PropertyTypeLive.Show, :show live "/custom_fields/:id", CustomFieldLive.Show, :show
live "/property_types/:id/show/edit", PropertyTypeLive.Show, :edit live "/custom_fields/:id/show/edit", CustomFieldLive.Show, :edit
live "/properties", PropertyLive.Index, :index live "/custom_field_values", CustomFieldValueLive.Index, :index
live "/properties/new", PropertyLive.Form, :new live "/custom_field_values/new", CustomFieldValueLive.Form, :new
live "/properties/:id/edit", PropertyLive.Form, :edit live "/custom_field_values/:id/edit", CustomFieldValueLive.Form, :edit
live "/properties/:id", PropertyLive.Show, :show live "/custom_field_values/:id", CustomFieldValueLive.Show, :show
live "/properties/:id/show/edit", PropertyLive.Show, :edit live "/custom_field_values/:id/show/edit", CustomFieldValueLive.Show, :edit
live "/users", UserLive.Index, :index live "/users", UserLive.Index, :index
live "/users/new", UserLive.Form, :new live "/users/new", UserLive.Form, :new

View file

@ -27,9 +27,9 @@ msgstr "Bist du sicher?"
msgid "Attempting to reconnect" msgid "Attempting to reconnect"
msgstr "Verbindung wird wiederhergestellt" msgstr "Verbindung wird wiederhergestellt"
#: lib/mv_web/live/member_live/form.ex:25 #: lib/mv_web/live/member_live/form.ex:54
#: lib/mv_web/live/member_live/index.html.heex:145 #: lib/mv_web/live/member_live/index.html.heex:145
#: lib/mv_web/live/member_live/show.ex:37 #: lib/mv_web/live/member_live/show.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "City" msgid "City"
msgstr "Stadt" msgstr "Stadt"
@ -41,43 +41,43 @@ msgid "Delete"
msgstr "Löschen" msgstr "Löschen"
#: lib/mv_web/live/member_live/index.html.heex:194 #: lib/mv_web/live/member_live/index.html.heex:194
#: lib/mv_web/live/user_live/form.ex:109 #: lib/mv_web/live/user_live/form.ex:141
#: lib/mv_web/live/user_live/index.html.heex:59 #: lib/mv_web/live/user_live/index.html.heex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit" msgid "Edit"
msgstr "Bearbeite" msgstr "Bearbeite"
#: lib/mv_web/live/member_live/show.ex:19 #: lib/mv_web/live/member_live/show.ex:41
#: lib/mv_web/live/member_live/show.ex:95 #: lib/mv_web/live/member_live/show.ex:117
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit Member" msgid "Edit Member"
msgstr "Mitglied bearbeiten" msgstr "Mitglied bearbeiten"
#: lib/mv_web/live/member_live/form.ex:18 #: lib/mv_web/live/member_live/form.ex:47
#: lib/mv_web/live/member_live/index.html.heex:77 #: lib/mv_web/live/member_live/index.html.heex:77
#: lib/mv_web/live/member_live/show.ex:28 #: lib/mv_web/live/member_live/show.ex:50
#: lib/mv_web/live/user_live/form.ex:14 #: lib/mv_web/live/user_live/form.ex:46
#: lib/mv_web/live/user_live/index.html.heex:44 #: lib/mv_web/live/user_live/index.html.heex:44
#: lib/mv_web/live/user_live/show.ex:25 #: lib/mv_web/live/user_live/show.ex:50
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Email" msgid "Email"
msgstr "E-Mail" msgstr "E-Mail"
#: lib/mv_web/live/member_live/form.ex:16 #: lib/mv_web/live/member_live/form.ex:45
#: lib/mv_web/live/member_live/show.ex:26 #: lib/mv_web/live/member_live/show.ex:48
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "First Name" msgid "First Name"
msgstr "Vorname" msgstr "Vorname"
#: lib/mv_web/live/member_live/form.ex:22 #: lib/mv_web/live/member_live/form.ex:51
#: lib/mv_web/live/member_live/index.html.heex:179 #: lib/mv_web/live/member_live/index.html.heex:179
#: lib/mv_web/live/member_live/show.ex:34 #: lib/mv_web/live/member_live/show.ex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Join Date" msgid "Join Date"
msgstr "Beitrittsdatum" msgstr "Beitrittsdatum"
#: lib/mv_web/live/member_live/form.ex:17 #: lib/mv_web/live/member_live/form.ex:46
#: lib/mv_web/live/member_live/show.ex:27 #: lib/mv_web/live/member_live/show.ex:49
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Last Name" msgid "Last Name"
msgstr "Nachname" msgstr "Nachname"
@ -108,117 +108,111 @@ msgstr "Keine Internetverbindung gefunden"
msgid "close" msgid "close"
msgstr "schließen" msgstr "schließen"
#: lib/mv_web/live/member_live/form.ex:19 #: lib/mv_web/live/member_live/form.ex:48
#: lib/mv_web/live/member_live/show.ex:29 #: lib/mv_web/live/member_live/show.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Birth Date" msgid "Birth Date"
msgstr "Geburtsdatum" msgstr "Geburtsdatum"
#: lib/mv_web/live/member_live/form.ex:30 #: lib/mv_web/live/member_live/form.ex:52
#: lib/mv_web/live/member_live/show.ex:56 #: lib/mv_web/live/member_live/show.ex:57
#, elixir-autogen, elixir-format
msgid "Custom Properties"
msgstr "Eigene Eigenschaften"
#: lib/mv_web/live/member_live/form.ex:23
#: lib/mv_web/live/member_live/show.ex:35
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Exit Date" msgid "Exit Date"
msgstr "Austrittsdatum" msgstr "Austrittsdatum"
#: lib/mv_web/live/member_live/form.ex:27 #: lib/mv_web/live/member_live/form.ex:56
#: lib/mv_web/live/member_live/index.html.heex:111 #: lib/mv_web/live/member_live/index.html.heex:111
#: lib/mv_web/live/member_live/show.ex:39 #: lib/mv_web/live/member_live/show.ex:61
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "House Number" msgid "House Number"
msgstr "Hausnummer" msgstr "Hausnummer"
#: lib/mv_web/live/member_live/form.ex:24 #: lib/mv_web/live/member_live/form.ex:53
#: lib/mv_web/live/member_live/show.ex:36 #: lib/mv_web/live/member_live/show.ex:58
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Notes" msgid "Notes"
msgstr "Notizen" msgstr "Notizen"
#: lib/mv_web/live/member_live/form.ex:20 #: lib/mv_web/live/member_live/form.ex:49
#: lib/mv_web/live/member_live/show.ex:30 #: lib/mv_web/live/member_live/show.ex:52
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Paid" msgid "Paid"
msgstr "Bezahlt" msgstr "Bezahlt"
#: lib/mv_web/live/member_live/form.ex:21 #: lib/mv_web/live/member_live/form.ex:50
#: lib/mv_web/live/member_live/index.html.heex:162 #: lib/mv_web/live/member_live/index.html.heex:162
#: lib/mv_web/live/member_live/show.ex:33 #: lib/mv_web/live/member_live/show.ex:55
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Phone Number" msgid "Phone Number"
msgstr "Telefonnummer" msgstr "Telefonnummer"
#: lib/mv_web/live/member_live/form.ex:28 #: lib/mv_web/live/member_live/form.ex:57
#: lib/mv_web/live/member_live/index.html.heex:128 #: lib/mv_web/live/member_live/index.html.heex:128
#: lib/mv_web/live/member_live/show.ex:40 #: lib/mv_web/live/member_live/show.ex:62
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Postal Code" msgid "Postal Code"
msgstr "Postleitzahl" msgstr "Postleitzahl"
#: lib/mv_web/live/member_live/form.ex:50 #: lib/mv_web/live/member_live/form.ex:80
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save Member" msgid "Save Member"
msgstr "Mitglied speichern" msgstr "Mitglied speichern"
#: lib/mv_web/live/member_live/form.ex:49 #: lib/mv_web/live/custom_field_live/form.ex:63
#: lib/mv_web/live/property_live/form.ex:41 #: lib/mv_web/live/custom_field_value_live/form.ex:74
#: lib/mv_web/live/property_type_live/form.ex:29 #: lib/mv_web/live/member_live/form.ex:79
#: lib/mv_web/live/user_live/form.ex:92 #: lib/mv_web/live/user_live/form.ex:124
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Saving..." msgid "Saving..."
msgstr "Speichern..." msgstr "Speichern..."
#: lib/mv_web/live/member_live/form.ex:26 #: lib/mv_web/live/member_live/form.ex:55
#: lib/mv_web/live/member_live/index.html.heex:94 #: lib/mv_web/live/member_live/index.html.heex:94
#: lib/mv_web/live/member_live/show.ex:38 #: lib/mv_web/live/member_live/show.ex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Street" msgid "Street"
msgstr "Straße" msgstr "Straße"
#: lib/mv_web/live/member_live/form.ex:11 #: lib/mv_web/live/member_live/form.ex:40
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Use this form to manage member records and their properties." msgid "Use this form to manage member records and their properties."
msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenschaften." msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenschaften."
#: lib/mv_web/live/member_live/show.ex:25 #: lib/mv_web/live/member_live/show.ex:47
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Id" msgid "Id"
msgstr "ID" msgstr "ID"
#: lib/mv_web/live/member_live/show.ex:31 #: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No" msgid "No"
msgstr "Nein" msgstr "Nein"
#: lib/mv_web/live/member_live/show.ex:94 #: lib/mv_web/live/member_live/show.ex:116
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Show Member" msgid "Show Member"
msgstr "Mitglied anzeigen" msgstr "Mitglied anzeigen"
#: lib/mv_web/live/member_live/show.ex:11 #: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "This is a member record from your database." msgid "This is a member record from your database."
msgstr "Dies ist ein Mitglied aus deiner Datenbank." msgstr "Dies ist ein Mitglied aus deiner Datenbank."
#: lib/mv_web/live/member_live/show.ex:31 #: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Yes" msgid "Yes"
msgstr "Ja" msgstr "Ja"
#: lib/mv_web/live/member_live/form.ex:108 #: lib/mv_web/live/custom_field_live/form.ex:107
#: lib/mv_web/live/property_live/form.ex:200 #: lib/mv_web/live/custom_field_value_live/form.ex:233
#: lib/mv_web/live/property_type_live/form.ex:73 #: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "create" msgid "create"
msgstr "erstellt" msgstr "erstellt"
#: lib/mv_web/live/member_live/form.ex:109 #: lib/mv_web/live/custom_field_live/form.ex:108
#: lib/mv_web/live/property_live/form.ex:201 #: lib/mv_web/live/custom_field_value_live/form.ex:234
#: lib/mv_web/live/property_type_live/form.ex:74 #: lib/mv_web/live/member_live/form.ex:139
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "update" msgid "update"
msgstr "aktualisiert" msgstr "aktualisiert"
@ -228,7 +222,7 @@ msgstr "aktualisiert"
msgid "Incorrect email or password" msgid "Incorrect email or password"
msgstr "Falsche E-Mail oder Passwort" msgstr "Falsche E-Mail oder Passwort"
#: lib/mv_web/live/member_live/form.ex:115 #: lib/mv_web/live/member_live/form.ex:145
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member %{action} successfully" msgid "Member %{action} successfully"
msgstr "Mitglied %{action} erfolgreich" msgstr "Mitglied %{action} erfolgreich"
@ -258,73 +252,68 @@ msgstr "Ihre E-Mail-Adresse wurde bestätigt"
msgid "Your password has successfully been reset" msgid "Your password has successfully been reset"
msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt" msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt"
#: lib/mv_web/live/member_live/form.ex:52 #: lib/mv_web/live/custom_field_live/form.ex:66
#: lib/mv_web/live/property_live/form.ex:44 #: lib/mv_web/live/custom_field_value_live/form.ex:77
#: lib/mv_web/live/property_type_live/form.ex:32 #: lib/mv_web/live/member_live/form.ex:82
#: lib/mv_web/live/user_live/form.ex:95 #: lib/mv_web/live/user_live/form.ex:127
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Cancel" msgid "Cancel"
msgstr "Abbrechen" msgstr "Abbrechen"
#: lib/mv_web/live/property_live/form.ex:29 #: lib/mv_web/live/custom_field_value_live/form.ex:62
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Choose a member" msgid "Choose a member"
msgstr "Mitglied auswählen" msgstr "Mitglied auswählen"
#: lib/mv_web/live/property_live/form.ex:20 #: lib/mv_web/live/custom_field_live/form.ex:59
#, elixir-autogen, elixir-format
msgid "Choose a property type"
msgstr "Eigenschaftstyp auswählen"
#: lib/mv_web/live/property_type_live/form.ex:25
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Description" msgid "Description"
msgstr "Beschreibung" msgstr "Beschreibung"
#: lib/mv_web/live/user_live/show.ex:18 #: lib/mv_web/live/user_live/show.ex:43
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit User" msgid "Edit User"
msgstr "Benutzer*in bearbeiten" msgstr "Benutzer*in bearbeiten"
#: lib/mv_web/live/user_live/show.ex:28 #: lib/mv_web/live/user_live/show.ex:53
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Enabled" msgid "Enabled"
msgstr "Aktiviert" msgstr "Aktiviert"
#: lib/mv_web/live/user_live/show.ex:24 #: lib/mv_web/live/user_live/show.ex:49
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "ID" msgid "ID"
msgstr "ID" msgstr "ID"
#: lib/mv_web/live/property_type_live/form.ex:26 #: lib/mv_web/live/custom_field_live/form.ex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Immutable" msgid "Immutable"
msgstr "Unveränderlich" msgstr "Unveränderlich"
#: lib/mv_web/components/layouts/navbar.ex:93 #: lib/mv_web/components/layouts/navbar.ex:94
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Logout" msgid "Logout"
msgstr "Abmelden" msgstr "Abmelden"
#: lib/mv_web/live/user_live/index.ex:12 #: lib/mv_web/live/user_live/index.ex:33
#: lib/mv_web/live/user_live/index.html.heex:3 #: lib/mv_web/live/user_live/index.html.heex:3
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Listing Users" msgid "Listing Users"
msgstr "Benutzer*innen auflisten" msgstr "Benutzer*innen auflisten"
#: lib/mv_web/live/property_live/form.ex:27 #: lib/mv_web/live/custom_field_value_live/form.ex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member" msgid "Member"
msgstr "Mitglied" msgstr "Mitglied"
#: lib/mv_web/components/layouts/navbar.ex:19 #: lib/mv_web/components/layouts/navbar.ex:19
#: lib/mv_web/live/member_live/index.ex:10 #: lib/mv_web/live/member_live/index.ex:39
#: lib/mv_web/live/member_live/index.html.heex:3 #: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Members" msgid "Members"
msgstr "Mitglieder" msgstr "Mitglieder"
#: lib/mv_web/live/property_type_live/form.ex:16 #: lib/mv_web/live/custom_field_live/form.ex:50
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Name" msgid "Name"
msgstr "Name" msgstr "Name"
@ -334,73 +323,43 @@ msgstr "Name"
msgid "New User" msgid "New User"
msgstr "Neue*r Benutzer*in" msgstr "Neue*r Benutzer*in"
#: lib/mv_web/live/user_live/show.ex:28 #: lib/mv_web/live/user_live/show.ex:53
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Not enabled" msgid "Not enabled"
msgstr "Nicht aktiviert" msgstr "Nicht aktiviert"
#: lib/mv_web/live/user_live/show.ex:26 #: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Not set" msgid "Not set"
msgstr "Nicht gesetzt" msgstr "Nicht gesetzt"
#: lib/mv_web/live/user_live/form.ex:75 #: lib/mv_web/live/user_live/form.ex:107
#: lib/mv_web/live/user_live/form.ex:83 #: lib/mv_web/live/user_live/form.ex:115
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Note" msgid "Note"
msgstr "Hinweis" msgstr "Hinweis"
#: lib/mv_web/live/user_live/index.html.heex:52 #: lib/mv_web/live/user_live/index.html.heex:52
#: lib/mv_web/live/user_live/show.ex:26 #: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "OIDC ID" msgid "OIDC ID"
msgstr "OIDC ID" msgstr "OIDC ID"
#: lib/mv_web/live/user_live/show.ex:27 #: lib/mv_web/live/user_live/show.ex:52
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Password Authentication" msgid "Password Authentication"
msgstr "Passwort-Authentifizierung" msgstr "Passwort-Authentifizierung"
#: lib/mv_web/live/property_live/form.ex:37 #: lib/mv_web/components/layouts/navbar.ex:89
#, elixir-autogen, elixir-format
msgid "Please select a property type first"
msgstr "Bitte wählen Sie zuerst einen Eigenschaftstyp"
#: lib/mv_web/components/layouts/navbar.ex:88
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Profil" msgid "Profil"
msgstr "Profil" msgstr "Profil"
#: lib/mv_web/live/property_live/form.ex:207 #: lib/mv_web/live/custom_field_live/form.ex:61
#, elixir-autogen, elixir-format
msgid "Property %{action} successfully"
msgstr "Eigenschaft %{action} erfolgreich"
#: lib/mv_web/live/property_live/form.ex:18
#, elixir-autogen, elixir-format
msgid "Property type"
msgstr "Eigenschaftstyp"
#: lib/mv_web/live/property_type_live/form.ex:80
#, elixir-autogen, elixir-format
msgid "Property type %{action} successfully"
msgstr "Eigenschaftstyp %{action} erfolgreich"
#: lib/mv_web/live/property_type_live/form.ex:27
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Required" msgid "Required"
msgstr "Erforderlich" msgstr "Erforderlich"
#: lib/mv_web/live/property_live/form.ex:42
#, elixir-autogen, elixir-format
msgid "Save Property"
msgstr "Eigenschaft speichern"
#: lib/mv_web/live/property_type_live/form.ex:30
#, elixir-autogen, elixir-format
msgid "Save Property type"
msgstr "Eigenschaftstyp speichern"
#: lib/mv_web/live/member_live/index.html.heex:34 #: lib/mv_web/live/member_live/index.html.heex:34
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Select all members" msgid "Select all members"
@ -411,58 +370,48 @@ msgstr "Alle Mitglieder auswählen"
msgid "Select member" msgid "Select member"
msgstr "Mitglied auswählen" msgstr "Mitglied auswählen"
#: lib/mv_web/components/layouts/navbar.ex:91 #: lib/mv_web/components/layouts/navbar.ex:92
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
msgstr "Einstellungen" msgstr "Einstellungen"
#: lib/mv_web/live/user_live/form.ex:93 #: lib/mv_web/live/user_live/form.ex:125
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save User" msgid "Save User"
msgstr "Benutzer*in speichern" msgstr "Benutzer*in speichern"
#: lib/mv_web/live/user_live/show.ex:54 #: lib/mv_web/live/user_live/show.ex:79
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Show User" msgid "Show User"
msgstr "Benutzer*in anzeigen" msgstr "Benutzer*in anzeigen"
#: lib/mv_web/live/user_live/show.ex:10 #: lib/mv_web/live/user_live/show.ex:35
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "This is a user record from your database." msgid "This is a user record from your database."
msgstr "Dies ist ein Benutzer*innen-Datensatz aus Ihrer Datenbank." msgstr "Dies ist ein Benutzer*innen-Datensatz aus Ihrer Datenbank."
#: lib/mv_web/live/property_live/form.ex:95 #: lib/mv_web/live/custom_field_value_live/form.ex:128
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unsupported value type: %{type}" msgid "Unsupported value type: %{type}"
msgstr "Nicht unterstützter Wertetyp: %{type}" msgstr "Nicht unterstützter Wertetyp: %{type}"
#: lib/mv_web/live/property_live/form.ex:10 #: lib/mv_web/live/user_live/form.ex:42
#, elixir-autogen, elixir-format
msgid "Use this form to manage property records in your database."
msgstr "Dieses Formular dient zur Verwaltung von Eigenschaften in der Datenbank."
#: lib/mv_web/live/property_type_live/form.ex:11
#, elixir-autogen, elixir-format
msgid "Use this form to manage property_type records in your database."
msgstr "Dieses Formular dient zur Verwaltung von Eigenschaftstypen in der Datenbank."
#: lib/mv_web/live/user_live/form.ex:10
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Use this form to manage user records in your database." msgid "Use this form to manage user records in your database."
msgstr "Verwenden Sie dieses Formular, um Benutzer*innen-Datensätze zu verwalten." msgstr "Verwenden Sie dieses Formular, um Benutzer*innen-Datensätze zu verwalten."
#: lib/mv_web/live/user_live/form.ex:110 #: lib/mv_web/live/user_live/form.ex:142
#: lib/mv_web/live/user_live/show.ex:9 #: lib/mv_web/live/user_live/show.ex:34
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "User" msgid "User"
msgstr "Benutzer*in" msgstr "Benutzer*in"
#: lib/mv_web/live/property_live/form.ex:59 #: lib/mv_web/live/custom_field_value_live/form.ex:92
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Value" msgid "Value"
msgstr "Wert" msgstr "Wert"
#: lib/mv_web/live/property_type_live/form.ex:20 #: lib/mv_web/live/custom_field_live/form.ex:54
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Value type" msgid "Value type"
msgstr "Wertetyp" msgstr "Wertetyp"
@ -479,57 +428,57 @@ msgstr "aufsteigend"
msgid "descending" msgid "descending"
msgstr "absteigend" msgstr "absteigend"
#: lib/mv_web/live/user_live/form.ex:109 #: lib/mv_web/live/user_live/form.ex:141
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "New" msgid "New"
msgstr "Neue*r" msgstr "Neue*r"
#: lib/mv_web/live/user_live/form.ex:64 #: lib/mv_web/live/user_live/form.ex:96
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Admin Note" msgid "Admin Note"
msgstr "Administrator*innen-Hinweis" msgstr "Administrator*innen-Hinweis"
#: lib/mv_web/live/user_live/form.ex:64 #: lib/mv_web/live/user_live/form.ex:96
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system." msgid "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
msgstr "Als Administrator*in können Sie direkt ein neues Passwort für diese*n Benutzer*in setzen, wobei das gleiche sichere Ash Authentication System verwendet wird." msgstr "Als Administrator*in können Sie direkt ein neues Passwort für diese*n Benutzer*in setzen, wobei das gleiche sichere Ash Authentication System verwendet wird."
#: lib/mv_web/live/user_live/form.ex:55 #: lib/mv_web/live/user_live/form.ex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "At least 8 characters" msgid "At least 8 characters"
msgstr "Mindestens 8 Zeichen" msgstr "Mindestens 8 Zeichen"
#: lib/mv_web/live/user_live/form.ex:27 #: lib/mv_web/live/user_live/form.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Change Password" msgid "Change Password"
msgstr "Passwort ändern" msgstr "Passwort ändern"
#: lib/mv_web/live/user_live/form.ex:75 #: lib/mv_web/live/user_live/form.ex:107
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Check 'Change Password' above to set a new password for this user." msgid "Check 'Change Password' above to set a new password for this user."
msgstr "Aktivieren Sie 'Passwort ändern' oben, um ein neues Passwort für diese*n Benutzer*in zu setzen." msgstr "Aktivieren Sie 'Passwort ändern' oben, um ein neues Passwort für diese*n Benutzer*in zu setzen."
#: lib/mv_web/live/user_live/form.ex:45 #: lib/mv_web/live/user_live/form.ex:77
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Confirm Password" msgid "Confirm Password"
msgstr "Passwort bestätigen" msgstr "Passwort bestätigen"
#: lib/mv_web/live/user_live/form.ex:57 #: lib/mv_web/live/user_live/form.ex:89
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Consider using special characters" msgid "Consider using special characters"
msgstr "Sonderzeichen empfohlen" msgstr "Sonderzeichen empfohlen"
#: lib/mv_web/live/user_live/form.ex:56 #: lib/mv_web/live/user_live/form.ex:88
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Include both letters and numbers" msgid "Include both letters and numbers"
msgstr "Buchstaben und Zahlen verwenden" msgstr "Buchstaben und Zahlen verwenden"
#: lib/mv_web/live/user_live/form.ex:35 #: lib/mv_web/live/user_live/form.ex:67
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Password" msgid "Password"
msgstr "Passwort" msgstr "Passwort"
#: lib/mv_web/live/user_live/form.ex:53 #: lib/mv_web/live/user_live/form.ex:85
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Password requirements" msgid "Password requirements"
msgstr "Passwort-Anforderungen" msgstr "Passwort-Anforderungen"
@ -544,56 +493,56 @@ msgstr "Alle Benutzer*innen auswählen"
msgid "Select user" msgid "Select user"
msgstr "Benutzer*in auswählen" msgstr "Benutzer*in auswählen"
#: lib/mv_web/live/user_live/form.ex:27 #: lib/mv_web/live/user_live/form.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Set Password" msgid "Set Password"
msgstr "Passwort setzen" msgstr "Passwort setzen"
#: lib/mv_web/live/user_live/form.ex:83 #: lib/mv_web/live/user_live/form.ex:115
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "User will be created without a password. Check 'Set Password' to add one." msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "Benutzer*in wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen', um eines hinzuzufügen." msgstr "Benutzer*in wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen', um eines hinzuzufügen."
#: lib/mv_web/live/user_live/show.ex:30 #: lib/mv_web/live/user_live/show.ex:55
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Linked Member" msgid "Linked Member"
msgstr "Verknüpftes Mitglied" msgstr "Verknüpftes Mitglied"
#: lib/mv_web/live/member_live/show.ex:41 #: lib/mv_web/live/member_live/show.ex:63
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Linked User" msgid "Linked User"
msgstr "Verknüpfte*r Benutzer*in" msgstr "Verknüpfte*r Benutzer*in"
#: lib/mv_web/live/user_live/show.ex:40 #: lib/mv_web/live/user_live/show.ex:65
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No member linked" msgid "No member linked"
msgstr "Kein Mitglied verknüpft" msgstr "Kein Mitglied verknüpft"
#: lib/mv_web/live/member_live/show.ex:51 #: lib/mv_web/live/member_live/show.ex:73
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No user linked" msgid "No user linked"
msgstr "Keine*r Benutzer*in verknüpft" msgstr "Keine*r Benutzer*in verknüpft"
#: lib/mv_web/live/member_live/show.ex:14 #: lib/mv_web/live/member_live/show.ex:36
#: lib/mv_web/live/member_live/show.ex:16 #: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Back to members list" msgid "Back to members list"
msgstr "Zurück zur Mitgliederliste" msgstr "Zurück zur Mitgliederliste"
#: lib/mv_web/live/user_live/show.ex:13 #: lib/mv_web/live/user_live/show.ex:38
#: lib/mv_web/live/user_live/show.ex:15 #: lib/mv_web/live/user_live/show.ex:40
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Back to users list" msgid "Back to users list"
msgstr "Zurück zur Benutzer*innen-Liste" msgstr "Zurück zur Benutzer*innen-Liste"
#: lib/mv_web/components/layouts/navbar.ex:26 #: lib/mv_web/components/layouts/navbar.ex:27
#: lib/mv_web/components/layouts/navbar.ex:32 #: lib/mv_web/components/layouts/navbar.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Select language" msgid "Select language"
msgstr "Sprache auswählen" msgstr "Sprache auswählen"
#: lib/mv_web/components/layouts/navbar.ex:39 #: lib/mv_web/components/layouts/navbar.ex:40
#: lib/mv_web/components/layouts/navbar.ex:59 #: lib/mv_web/components/layouts/navbar.ex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Toggle dark mode" msgid "Toggle dark mode"
msgstr "Dunklen Modus umschalten" msgstr "Dunklen Modus umschalten"
@ -604,7 +553,7 @@ msgstr "Dunklen Modus umschalten"
msgid "Search..." msgid "Search..."
msgstr "Suchen..." msgstr "Suchen..."
#: lib/mv_web/components/layouts/navbar.ex:20 #: lib/mv_web/components/layouts/navbar.ex:21
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Users" msgid "Users"
msgstr "Benutzer*innen" msgstr "Benutzer*innen"
@ -650,3 +599,59 @@ msgstr "E-Mail kann nicht aktualisiert werden: Diese E-Mail-Adresse ist bereits
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account." msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
msgstr "Diese E-Mail-Adresse ist bereits mit einem anderen OIDC-Konto verknüpft. Es können nicht mehrere OIDC-Provider mit demselben Konto verknüpft werden." msgstr "Diese E-Mail-Adresse ist bereits mit einem anderen OIDC-Konto verknüpft. Es können nicht mehrere OIDC-Provider mit demselben Konto verknüpft werden."
#: lib/mv_web/live/custom_field_value_live/form.ex:53
#, elixir-autogen, elixir-format
msgid "Choose a custom field"
msgstr "Wähle ein Benutzerdefiniertes Feld"
#: lib/mv_web/live/member_live/form.ex:59
#: lib/mv_web/live/member_live/show.ex:78
#, elixir-autogen, elixir-format
msgid "Custom Field Values"
msgstr "Benutzerdefinierte Feldwerte"
#: lib/mv_web/live/custom_field_value_live/form.ex:51
#, elixir-autogen, elixir-format
msgid "Custom field"
msgstr "Benutzerdefiniertes Feld"
#: lib/mv_web/live/custom_field_live/form.ex:114
#, elixir-autogen, elixir-format
msgid "Custom field %{action} successfully"
msgstr "Benutzerdefiniertes Feld erfolgreich %{action}"
#: lib/mv_web/live/custom_field_value_live/form.ex:242
#, elixir-autogen, elixir-format
msgid "Custom field value %{action} successfully"
msgstr "Benutzerdefinierter Feldwert erfolgreich %{action}"
#: lib/mv_web/live/custom_field_value_live/form.ex:70
#, elixir-autogen, elixir-format
msgid "Please select a custom field first"
msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld"
#: lib/mv_web/live/custom_field_live/form.ex:64
#, elixir-autogen, elixir-format
msgid "Save Custom field"
msgstr "Benutzerdefiniertes Feld speichern"
#: lib/mv_web/live/custom_field_value_live/form.ex:75
#, elixir-autogen, elixir-format
msgid "Save Custom field value"
msgstr "Benutzerdefinierten Feldwert speichern"
#: lib/mv_web/live/custom_field_live/form.ex:45
#, 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"

View file

@ -28,9 +28,9 @@ msgstr ""
msgid "Attempting to reconnect" msgid "Attempting to reconnect"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:25 #: lib/mv_web/live/member_live/form.ex:54
#: lib/mv_web/live/member_live/index.html.heex:145 #: lib/mv_web/live/member_live/index.html.heex:145
#: lib/mv_web/live/member_live/show.ex:37 #: lib/mv_web/live/member_live/show.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "City" msgid "City"
msgstr "" msgstr ""
@ -42,43 +42,43 @@ msgid "Delete"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:194 #: lib/mv_web/live/member_live/index.html.heex:194
#: lib/mv_web/live/user_live/form.ex:109 #: lib/mv_web/live/user_live/form.ex:141
#: lib/mv_web/live/user_live/index.html.heex:59 #: lib/mv_web/live/user_live/index.html.heex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:19 #: lib/mv_web/live/member_live/show.ex:41
#: lib/mv_web/live/member_live/show.ex:95 #: lib/mv_web/live/member_live/show.ex:117
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit Member" msgid "Edit Member"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:18 #: lib/mv_web/live/member_live/form.ex:47
#: lib/mv_web/live/member_live/index.html.heex:77 #: lib/mv_web/live/member_live/index.html.heex:77
#: lib/mv_web/live/member_live/show.ex:28 #: lib/mv_web/live/member_live/show.ex:50
#: lib/mv_web/live/user_live/form.ex:14 #: lib/mv_web/live/user_live/form.ex:46
#: lib/mv_web/live/user_live/index.html.heex:44 #: lib/mv_web/live/user_live/index.html.heex:44
#: lib/mv_web/live/user_live/show.ex:25 #: lib/mv_web/live/user_live/show.ex:50
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Email" msgid "Email"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:16 #: lib/mv_web/live/member_live/form.ex:45
#: lib/mv_web/live/member_live/show.ex:26 #: lib/mv_web/live/member_live/show.ex:48
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "First Name" msgid "First Name"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:22 #: lib/mv_web/live/member_live/form.ex:51
#: lib/mv_web/live/member_live/index.html.heex:179 #: lib/mv_web/live/member_live/index.html.heex:179
#: lib/mv_web/live/member_live/show.ex:34 #: lib/mv_web/live/member_live/show.ex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Join Date" msgid "Join Date"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:17 #: lib/mv_web/live/member_live/form.ex:46
#: lib/mv_web/live/member_live/show.ex:27 #: lib/mv_web/live/member_live/show.ex:49
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Last Name" msgid "Last Name"
msgstr "" msgstr ""
@ -109,117 +109,111 @@ msgstr ""
msgid "close" msgid "close"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:19 #: lib/mv_web/live/member_live/form.ex:48
#: lib/mv_web/live/member_live/show.ex:29 #: lib/mv_web/live/member_live/show.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Birth Date" msgid "Birth Date"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:30 #: lib/mv_web/live/member_live/form.ex:52
#: lib/mv_web/live/member_live/show.ex:56 #: lib/mv_web/live/member_live/show.ex:57
#, elixir-autogen, elixir-format
msgid "Custom Properties"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:23
#: lib/mv_web/live/member_live/show.ex:35
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Exit Date" msgid "Exit Date"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:27 #: lib/mv_web/live/member_live/form.ex:56
#: lib/mv_web/live/member_live/index.html.heex:111 #: lib/mv_web/live/member_live/index.html.heex:111
#: lib/mv_web/live/member_live/show.ex:39 #: lib/mv_web/live/member_live/show.ex:61
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "House Number" msgid "House Number"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:24 #: lib/mv_web/live/member_live/form.ex:53
#: lib/mv_web/live/member_live/show.ex:36 #: lib/mv_web/live/member_live/show.ex:58
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Notes" msgid "Notes"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:20 #: lib/mv_web/live/member_live/form.ex:49
#: lib/mv_web/live/member_live/show.ex:30 #: lib/mv_web/live/member_live/show.ex:52
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Paid" msgid "Paid"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:21 #: lib/mv_web/live/member_live/form.ex:50
#: lib/mv_web/live/member_live/index.html.heex:162 #: lib/mv_web/live/member_live/index.html.heex:162
#: lib/mv_web/live/member_live/show.ex:33 #: lib/mv_web/live/member_live/show.ex:55
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Phone Number" msgid "Phone Number"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:28 #: lib/mv_web/live/member_live/form.ex:57
#: lib/mv_web/live/member_live/index.html.heex:128 #: lib/mv_web/live/member_live/index.html.heex:128
#: lib/mv_web/live/member_live/show.ex:40 #: lib/mv_web/live/member_live/show.ex:62
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Postal Code" msgid "Postal Code"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:50 #: lib/mv_web/live/member_live/form.ex:80
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save Member" msgid "Save Member"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:49 #: lib/mv_web/live/custom_field_live/form.ex:63
#: lib/mv_web/live/property_live/form.ex:41 #: lib/mv_web/live/custom_field_value_live/form.ex:74
#: lib/mv_web/live/property_type_live/form.ex:29 #: lib/mv_web/live/member_live/form.ex:79
#: lib/mv_web/live/user_live/form.ex:92 #: lib/mv_web/live/user_live/form.ex:124
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Saving..." msgid "Saving..."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:26 #: lib/mv_web/live/member_live/form.ex:55
#: lib/mv_web/live/member_live/index.html.heex:94 #: lib/mv_web/live/member_live/index.html.heex:94
#: lib/mv_web/live/member_live/show.ex:38 #: lib/mv_web/live/member_live/show.ex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Street" msgid "Street"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:11 #: lib/mv_web/live/member_live/form.ex:40
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Use this form to manage member records and their properties." msgid "Use this form to manage member records and their properties."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:25 #: lib/mv_web/live/member_live/show.ex:47
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Id" msgid "Id"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:31 #: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No" msgid "No"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:94 #: lib/mv_web/live/member_live/show.ex:116
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Show Member" msgid "Show Member"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:11 #: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "This is a member record from your database." msgid "This is a member record from your database."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:31 #: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:108 #: lib/mv_web/live/custom_field_live/form.ex:107
#: lib/mv_web/live/property_live/form.ex:200 #: lib/mv_web/live/custom_field_value_live/form.ex:233
#: lib/mv_web/live/property_type_live/form.ex:73 #: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "create" msgid "create"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:109 #: lib/mv_web/live/custom_field_live/form.ex:108
#: lib/mv_web/live/property_live/form.ex:201 #: lib/mv_web/live/custom_field_value_live/form.ex:234
#: lib/mv_web/live/property_type_live/form.ex:74 #: lib/mv_web/live/member_live/form.ex:139
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "update" msgid "update"
msgstr "" msgstr ""
@ -229,7 +223,7 @@ msgstr ""
msgid "Incorrect email or password" msgid "Incorrect email or password"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:115 #: lib/mv_web/live/member_live/form.ex:145
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member %{action} successfully" msgid "Member %{action} successfully"
msgstr "" msgstr ""
@ -259,73 +253,68 @@ msgstr ""
msgid "Your password has successfully been reset" msgid "Your password has successfully been reset"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:52 #: lib/mv_web/live/custom_field_live/form.ex:66
#: lib/mv_web/live/property_live/form.ex:44 #: lib/mv_web/live/custom_field_value_live/form.ex:77
#: lib/mv_web/live/property_type_live/form.ex:32 #: lib/mv_web/live/member_live/form.ex:82
#: lib/mv_web/live/user_live/form.ex:95 #: lib/mv_web/live/user_live/form.ex:127
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:29 #: lib/mv_web/live/custom_field_value_live/form.ex:62
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Choose a member" msgid "Choose a member"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:20 #: lib/mv_web/live/custom_field_live/form.ex:59
#, elixir-autogen, elixir-format
msgid "Choose a property type"
msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:25
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Description" msgid "Description"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:18 #: lib/mv_web/live/user_live/show.ex:43
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit User" msgid "Edit User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:28 #: lib/mv_web/live/user_live/show.ex:53
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Enabled" msgid "Enabled"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:24 #: lib/mv_web/live/user_live/show.ex:49
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "ID" msgid "ID"
msgstr "" msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:26 #: lib/mv_web/live/custom_field_live/form.ex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Immutable" msgid "Immutable"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:93 #: lib/mv_web/components/layouts/navbar.ex:94
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Logout" msgid "Logout"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/index.ex:12 #: lib/mv_web/live/user_live/index.ex:33
#: lib/mv_web/live/user_live/index.html.heex:3 #: lib/mv_web/live/user_live/index.html.heex:3
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Listing Users" msgid "Listing Users"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:27 #: lib/mv_web/live/custom_field_value_live/form.ex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member" msgid "Member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:19 #: lib/mv_web/components/layouts/navbar.ex:19
#: lib/mv_web/live/member_live/index.ex:10 #: lib/mv_web/live/member_live/index.ex:39
#: lib/mv_web/live/member_live/index.html.heex:3 #: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Members" msgid "Members"
msgstr "" msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:16 #: lib/mv_web/live/custom_field_live/form.ex:50
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Name" msgid "Name"
msgstr "" msgstr ""
@ -335,73 +324,43 @@ msgstr ""
msgid "New User" msgid "New User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:28 #: lib/mv_web/live/user_live/show.ex:53
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Not enabled" msgid "Not enabled"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:26 #: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Not set" msgid "Not set"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:75 #: lib/mv_web/live/user_live/form.ex:107
#: lib/mv_web/live/user_live/form.ex:83 #: lib/mv_web/live/user_live/form.ex:115
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Note" msgid "Note"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/index.html.heex:52 #: lib/mv_web/live/user_live/index.html.heex:52
#: lib/mv_web/live/user_live/show.ex:26 #: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "OIDC ID" msgid "OIDC ID"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:27 #: lib/mv_web/live/user_live/show.ex:52
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Password Authentication" msgid "Password Authentication"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:37 #: lib/mv_web/components/layouts/navbar.ex:89
#, elixir-autogen, elixir-format
msgid "Please select a property type first"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:88
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Profil" msgid "Profil"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:207 #: lib/mv_web/live/custom_field_live/form.ex:61
#, elixir-autogen, elixir-format
msgid "Property %{action} successfully"
msgstr ""
#: lib/mv_web/live/property_live/form.ex:18
#, elixir-autogen, elixir-format
msgid "Property type"
msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:80
#, elixir-autogen, elixir-format
msgid "Property type %{action} successfully"
msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:27
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Required" msgid "Required"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:42
#, elixir-autogen, elixir-format
msgid "Save Property"
msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:30
#, elixir-autogen, elixir-format
msgid "Save Property type"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:34 #: lib/mv_web/live/member_live/index.html.heex:34
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Select all members" msgid "Select all members"
@ -412,58 +371,48 @@ msgstr ""
msgid "Select member" msgid "Select member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:91 #: lib/mv_web/components/layouts/navbar.ex:92
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:93 #: lib/mv_web/live/user_live/form.ex:125
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save User" msgid "Save User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:54 #: lib/mv_web/live/user_live/show.ex:79
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Show User" msgid "Show User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:10 #: lib/mv_web/live/user_live/show.ex:35
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "This is a user record from your database." msgid "This is a user record from your database."
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:95 #: lib/mv_web/live/custom_field_value_live/form.ex:128
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unsupported value type: %{type}" msgid "Unsupported value type: %{type}"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:10 #: lib/mv_web/live/user_live/form.ex:42
#, elixir-autogen, elixir-format
msgid "Use this form to manage property records in your database."
msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:11
#, elixir-autogen, elixir-format
msgid "Use this form to manage property_type records in your database."
msgstr ""
#: lib/mv_web/live/user_live/form.ex:10
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Use this form to manage user records in your database." msgid "Use this form to manage user records in your database."
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:110 #: lib/mv_web/live/user_live/form.ex:142
#: lib/mv_web/live/user_live/show.ex:9 #: lib/mv_web/live/user_live/show.ex:34
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "User" msgid "User"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:59 #: lib/mv_web/live/custom_field_value_live/form.ex:92
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Value" msgid "Value"
msgstr "" msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:20 #: lib/mv_web/live/custom_field_live/form.ex:54
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Value type" msgid "Value type"
msgstr "" msgstr ""
@ -480,57 +429,57 @@ msgstr ""
msgid "descending" msgid "descending"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:109 #: lib/mv_web/live/user_live/form.ex:141
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "New" msgid "New"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:64 #: lib/mv_web/live/user_live/form.ex:96
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Admin Note" msgid "Admin Note"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:64 #: lib/mv_web/live/user_live/form.ex:96
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system." msgid "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:55 #: lib/mv_web/live/user_live/form.ex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "At least 8 characters" msgid "At least 8 characters"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:27 #: lib/mv_web/live/user_live/form.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Change Password" msgid "Change Password"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:75 #: lib/mv_web/live/user_live/form.ex:107
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Check 'Change Password' above to set a new password for this user." msgid "Check 'Change Password' above to set a new password for this user."
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:45 #: lib/mv_web/live/user_live/form.ex:77
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Confirm Password" msgid "Confirm Password"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:57 #: lib/mv_web/live/user_live/form.ex:89
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Consider using special characters" msgid "Consider using special characters"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:56 #: lib/mv_web/live/user_live/form.ex:88
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Include both letters and numbers" msgid "Include both letters and numbers"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:35 #: lib/mv_web/live/user_live/form.ex:67
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Password" msgid "Password"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:53 #: lib/mv_web/live/user_live/form.ex:85
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Password requirements" msgid "Password requirements"
msgstr "" msgstr ""
@ -545,56 +494,56 @@ msgstr ""
msgid "Select user" msgid "Select user"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:27 #: lib/mv_web/live/user_live/form.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Set Password" msgid "Set Password"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:83 #: lib/mv_web/live/user_live/form.ex:115
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "User will be created without a password. Check 'Set Password' to add one." msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:30 #: lib/mv_web/live/user_live/show.ex:55
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Linked Member" msgid "Linked Member"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:41 #: lib/mv_web/live/member_live/show.ex:63
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Linked User" msgid "Linked User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:40 #: lib/mv_web/live/user_live/show.ex:65
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No member linked" msgid "No member linked"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:51 #: lib/mv_web/live/member_live/show.ex:73
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No user linked" msgid "No user linked"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:14 #: lib/mv_web/live/member_live/show.ex:36
#: lib/mv_web/live/member_live/show.ex:16 #: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Back to members list" msgid "Back to members list"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:13 #: lib/mv_web/live/user_live/show.ex:38
#: lib/mv_web/live/user_live/show.ex:15 #: lib/mv_web/live/user_live/show.ex:40
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Back to users list" msgid "Back to users list"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:26 #: lib/mv_web/components/layouts/navbar.ex:27
#: lib/mv_web/components/layouts/navbar.ex:32 #: lib/mv_web/components/layouts/navbar.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Select language" msgid "Select language"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:39 #: lib/mv_web/components/layouts/navbar.ex:40
#: lib/mv_web/components/layouts/navbar.ex:59 #: lib/mv_web/components/layouts/navbar.ex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Toggle dark mode" msgid "Toggle dark mode"
msgstr "" msgstr ""
@ -605,7 +554,7 @@ msgstr ""
msgid "Search..." msgid "Search..."
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:20 #: lib/mv_web/components/layouts/navbar.ex:21
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Users" msgid "Users"
msgstr "" msgstr ""
@ -651,3 +600,59 @@ msgstr ""
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account." msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:53
#, elixir-autogen, elixir-format
msgid "Choose a custom field"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:59
#: lib/mv_web/live/member_live/show.ex:78
#, elixir-autogen, elixir-format
msgid "Custom Field Values"
msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:51
#, elixir-autogen, elixir-format
msgid "Custom field"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:114
#, elixir-autogen, elixir-format
msgid "Custom field %{action} successfully"
msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:242
#, elixir-autogen, elixir-format
msgid "Custom field value %{action} successfully"
msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:70
#, elixir-autogen, elixir-format
msgid "Please select a custom field first"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:64
#, elixir-autogen, elixir-format
msgid "Save Custom field"
msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:75
#, elixir-autogen, elixir-format
msgid "Save Custom field value"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:45
#, 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 ""

View file

@ -28,9 +28,9 @@ msgstr ""
msgid "Attempting to reconnect" msgid "Attempting to reconnect"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:25 #: lib/mv_web/live/member_live/form.ex:54
#: lib/mv_web/live/member_live/index.html.heex:145 #: lib/mv_web/live/member_live/index.html.heex:145
#: lib/mv_web/live/member_live/show.ex:37 #: lib/mv_web/live/member_live/show.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "City" msgid "City"
msgstr "" msgstr ""
@ -42,43 +42,43 @@ msgid "Delete"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:194 #: lib/mv_web/live/member_live/index.html.heex:194
#: lib/mv_web/live/user_live/form.ex:109 #: lib/mv_web/live/user_live/form.ex:141
#: lib/mv_web/live/user_live/index.html.heex:59 #: lib/mv_web/live/user_live/index.html.heex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:19 #: lib/mv_web/live/member_live/show.ex:41
#: lib/mv_web/live/member_live/show.ex:95 #: lib/mv_web/live/member_live/show.ex:117
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit Member" msgid "Edit Member"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:18 #: lib/mv_web/live/member_live/form.ex:47
#: lib/mv_web/live/member_live/index.html.heex:77 #: lib/mv_web/live/member_live/index.html.heex:77
#: lib/mv_web/live/member_live/show.ex:28 #: lib/mv_web/live/member_live/show.ex:50
#: lib/mv_web/live/user_live/form.ex:14 #: lib/mv_web/live/user_live/form.ex:46
#: lib/mv_web/live/user_live/index.html.heex:44 #: lib/mv_web/live/user_live/index.html.heex:44
#: lib/mv_web/live/user_live/show.ex:25 #: lib/mv_web/live/user_live/show.ex:50
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Email" msgid "Email"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:16 #: lib/mv_web/live/member_live/form.ex:45
#: lib/mv_web/live/member_live/show.ex:26 #: lib/mv_web/live/member_live/show.ex:48
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "First Name" msgid "First Name"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:22 #: lib/mv_web/live/member_live/form.ex:51
#: lib/mv_web/live/member_live/index.html.heex:179 #: lib/mv_web/live/member_live/index.html.heex:179
#: lib/mv_web/live/member_live/show.ex:34 #: lib/mv_web/live/member_live/show.ex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Join Date" msgid "Join Date"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:17 #: lib/mv_web/live/member_live/form.ex:46
#: lib/mv_web/live/member_live/show.ex:27 #: lib/mv_web/live/member_live/show.ex:49
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Last Name" msgid "Last Name"
msgstr "" msgstr ""
@ -109,117 +109,111 @@ msgstr ""
msgid "close" msgid "close"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:19 #: lib/mv_web/live/member_live/form.ex:48
#: lib/mv_web/live/member_live/show.ex:29 #: lib/mv_web/live/member_live/show.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Birth Date" msgid "Birth Date"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:30 #: lib/mv_web/live/member_live/form.ex:52
#: lib/mv_web/live/member_live/show.ex:56 #: lib/mv_web/live/member_live/show.ex:57
#, elixir-autogen, elixir-format
msgid "Custom Properties"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:23
#: lib/mv_web/live/member_live/show.ex:35
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Exit Date" msgid "Exit Date"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:27 #: lib/mv_web/live/member_live/form.ex:56
#: lib/mv_web/live/member_live/index.html.heex:111 #: lib/mv_web/live/member_live/index.html.heex:111
#: lib/mv_web/live/member_live/show.ex:39 #: lib/mv_web/live/member_live/show.ex:61
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "House Number" msgid "House Number"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:24 #: lib/mv_web/live/member_live/form.ex:53
#: lib/mv_web/live/member_live/show.ex:36 #: lib/mv_web/live/member_live/show.ex:58
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Notes" msgid "Notes"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:20 #: lib/mv_web/live/member_live/form.ex:49
#: lib/mv_web/live/member_live/show.ex:30 #: lib/mv_web/live/member_live/show.ex:52
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Paid" msgid "Paid"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:21 #: lib/mv_web/live/member_live/form.ex:50
#: lib/mv_web/live/member_live/index.html.heex:162 #: lib/mv_web/live/member_live/index.html.heex:162
#: lib/mv_web/live/member_live/show.ex:33 #: lib/mv_web/live/member_live/show.ex:55
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Phone Number" msgid "Phone Number"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:28 #: lib/mv_web/live/member_live/form.ex:57
#: lib/mv_web/live/member_live/index.html.heex:128 #: lib/mv_web/live/member_live/index.html.heex:128
#: lib/mv_web/live/member_live/show.ex:40 #: lib/mv_web/live/member_live/show.ex:62
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Postal Code" msgid "Postal Code"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:50 #: lib/mv_web/live/member_live/form.ex:80
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Save Member" msgid "Save Member"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:49 #: lib/mv_web/live/custom_field_live/form.ex:63
#: lib/mv_web/live/property_live/form.ex:41 #: lib/mv_web/live/custom_field_value_live/form.ex:74
#: lib/mv_web/live/property_type_live/form.ex:29 #: lib/mv_web/live/member_live/form.ex:79
#: lib/mv_web/live/user_live/form.ex:92 #: lib/mv_web/live/user_live/form.ex:124
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Saving..." msgid "Saving..."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:26 #: lib/mv_web/live/member_live/form.ex:55
#: lib/mv_web/live/member_live/index.html.heex:94 #: lib/mv_web/live/member_live/index.html.heex:94
#: lib/mv_web/live/member_live/show.ex:38 #: lib/mv_web/live/member_live/show.ex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Street" msgid "Street"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:11 #: lib/mv_web/live/member_live/form.ex:40
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Use this form to manage member records and their properties." msgid "Use this form to manage member records and their properties."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:25 #: lib/mv_web/live/member_live/show.ex:47
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Id" msgid "Id"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:31 #: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No" msgid "No"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:94 #: lib/mv_web/live/member_live/show.ex:116
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Show Member" msgid "Show Member"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:11 #: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "This is a member record from your database." msgid "This is a member record from your database."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:31 #: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:108 #: lib/mv_web/live/custom_field_live/form.ex:107
#: lib/mv_web/live/property_live/form.ex:200 #: lib/mv_web/live/custom_field_value_live/form.ex:233
#: lib/mv_web/live/property_type_live/form.ex:73 #: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "create" msgid "create"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:109 #: lib/mv_web/live/custom_field_live/form.ex:108
#: lib/mv_web/live/property_live/form.ex:201 #: lib/mv_web/live/custom_field_value_live/form.ex:234
#: lib/mv_web/live/property_type_live/form.ex:74 #: lib/mv_web/live/member_live/form.ex:139
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "update" msgid "update"
msgstr "" msgstr ""
@ -229,7 +223,7 @@ msgstr ""
msgid "Incorrect email or password" msgid "Incorrect email or password"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:115 #: lib/mv_web/live/member_live/form.ex:145
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member %{action} successfully" msgid "Member %{action} successfully"
msgstr "" msgstr ""
@ -259,73 +253,68 @@ msgstr ""
msgid "Your password has successfully been reset" msgid "Your password has successfully been reset"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:52 #: lib/mv_web/live/custom_field_live/form.ex:66
#: lib/mv_web/live/property_live/form.ex:44 #: lib/mv_web/live/custom_field_value_live/form.ex:77
#: lib/mv_web/live/property_type_live/form.ex:32 #: lib/mv_web/live/member_live/form.ex:82
#: lib/mv_web/live/user_live/form.ex:95 #: lib/mv_web/live/user_live/form.ex:127
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:29 #: lib/mv_web/live/custom_field_value_live/form.ex:62
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Choose a member" msgid "Choose a member"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:20 #: lib/mv_web/live/custom_field_live/form.ex:59
#, elixir-autogen, elixir-format
msgid "Choose a property type"
msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:25
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Description" msgid "Description"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:18 #: lib/mv_web/live/user_live/show.ex:43
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Edit User" msgid "Edit User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:28 #: lib/mv_web/live/user_live/show.ex:53
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Enabled" msgid "Enabled"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:24 #: lib/mv_web/live/user_live/show.ex:49
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "ID" msgid "ID"
msgstr "" msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:26 #: lib/mv_web/live/custom_field_live/form.ex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Immutable" msgid "Immutable"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:93 #: lib/mv_web/components/layouts/navbar.ex:94
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Logout" msgid "Logout"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/index.ex:12 #: lib/mv_web/live/user_live/index.ex:33
#: lib/mv_web/live/user_live/index.html.heex:3 #: lib/mv_web/live/user_live/index.html.heex:3
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Listing Users" msgid "Listing Users"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:27 #: lib/mv_web/live/custom_field_value_live/form.ex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Member" msgid "Member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:19 #: lib/mv_web/components/layouts/navbar.ex:19
#: lib/mv_web/live/member_live/index.ex:10 #: lib/mv_web/live/member_live/index.ex:39
#: lib/mv_web/live/member_live/index.html.heex:3 #: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Members" msgid "Members"
msgstr "" msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:16 #: lib/mv_web/live/custom_field_live/form.ex:50
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Name" msgid "Name"
msgstr "" msgstr ""
@ -335,73 +324,43 @@ msgstr ""
msgid "New User" msgid "New User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:28 #: lib/mv_web/live/user_live/show.ex:53
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Not enabled" msgid "Not enabled"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:26 #: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Not set" msgid "Not set"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:75 #: lib/mv_web/live/user_live/form.ex:107
#: lib/mv_web/live/user_live/form.ex:83 #: lib/mv_web/live/user_live/form.ex:115
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Note" msgid "Note"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/index.html.heex:52 #: lib/mv_web/live/user_live/index.html.heex:52
#: lib/mv_web/live/user_live/show.ex:26 #: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "OIDC ID" msgid "OIDC ID"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:27 #: lib/mv_web/live/user_live/show.ex:52
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Password Authentication" msgid "Password Authentication"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:37 #: lib/mv_web/components/layouts/navbar.ex:89
#, elixir-autogen, elixir-format
msgid "Please select a property type first"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:88
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Profil" msgid "Profil"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:207 #: lib/mv_web/live/custom_field_live/form.ex:61
#, elixir-autogen, elixir-format, fuzzy
msgid "Property %{action} successfully"
msgstr ""
#: lib/mv_web/live/property_live/form.ex:18
#, elixir-autogen, elixir-format
msgid "Property type"
msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:80
#, elixir-autogen, elixir-format
msgid "Property type %{action} successfully"
msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:27
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Required" msgid "Required"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:42
#, elixir-autogen, elixir-format
msgid "Save Property"
msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:30
#, elixir-autogen, elixir-format
msgid "Save Property type"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:34 #: lib/mv_web/live/member_live/index.html.heex:34
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Select all members" msgid "Select all members"
@ -412,58 +371,48 @@ msgstr ""
msgid "Select member" msgid "Select member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:91 #: lib/mv_web/components/layouts/navbar.ex:92
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:93 #: lib/mv_web/live/user_live/form.ex:125
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Save User" msgid "Save User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:54 #: lib/mv_web/live/user_live/show.ex:79
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Show User" msgid "Show User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:10 #: lib/mv_web/live/user_live/show.ex:35
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "This is a user record from your database." msgid "This is a user record from your database."
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:95 #: lib/mv_web/live/custom_field_value_live/form.ex:128
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unsupported value type: %{type}" msgid "Unsupported value type: %{type}"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:10 #: lib/mv_web/live/user_live/form.ex:42
#, elixir-autogen, elixir-format, fuzzy
msgid "Use this form to manage property records in your database."
msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:11
#, elixir-autogen, elixir-format, fuzzy
msgid "Use this form to manage property_type records in your database."
msgstr ""
#: lib/mv_web/live/user_live/form.ex:10
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Use this form to manage user records in your database." msgid "Use this form to manage user records in your database."
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:110 #: lib/mv_web/live/user_live/form.ex:142
#: lib/mv_web/live/user_live/show.ex:9 #: lib/mv_web/live/user_live/show.ex:34
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "User" msgid "User"
msgstr "" msgstr ""
#: lib/mv_web/live/property_live/form.ex:59 #: lib/mv_web/live/custom_field_value_live/form.ex:92
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Value" msgid "Value"
msgstr "" msgstr ""
#: lib/mv_web/live/property_type_live/form.ex:20 #: lib/mv_web/live/custom_field_live/form.ex:54
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Value type" msgid "Value type"
msgstr "" msgstr ""
@ -480,57 +429,57 @@ msgstr ""
msgid "descending" msgid "descending"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:109 #: lib/mv_web/live/user_live/form.ex:141
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "New" msgid "New"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:64 #: lib/mv_web/live/user_live/form.ex:96
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Admin Note" msgid "Admin Note"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:64 #: lib/mv_web/live/user_live/form.ex:96
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system." msgid "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
msgstr "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system." msgstr "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
#: lib/mv_web/live/user_live/form.ex:55 #: lib/mv_web/live/user_live/form.ex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "At least 8 characters" msgid "At least 8 characters"
msgstr "At least 8 characters" msgstr "At least 8 characters"
#: lib/mv_web/live/user_live/form.ex:27 #: lib/mv_web/live/user_live/form.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Change Password" msgid "Change Password"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:75 #: lib/mv_web/live/user_live/form.ex:107
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Check 'Change Password' above to set a new password for this user." msgid "Check 'Change Password' above to set a new password for this user."
msgstr "Check 'Change Password' above to set a new password for this user." msgstr "Check 'Change Password' above to set a new password for this user."
#: lib/mv_web/live/user_live/form.ex:45 #: lib/mv_web/live/user_live/form.ex:77
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Confirm Password" msgid "Confirm Password"
msgstr "Confirm Password" msgstr "Confirm Password"
#: lib/mv_web/live/user_live/form.ex:57 #: lib/mv_web/live/user_live/form.ex:89
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Consider using special characters" msgid "Consider using special characters"
msgstr "Consider using special characters" msgstr "Consider using special characters"
#: lib/mv_web/live/user_live/form.ex:56 #: lib/mv_web/live/user_live/form.ex:88
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Include both letters and numbers" msgid "Include both letters and numbers"
msgstr "Include both letters and numbers" msgstr "Include both letters and numbers"
#: lib/mv_web/live/user_live/form.ex:35 #: lib/mv_web/live/user_live/form.ex:67
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Password" msgid "Password"
msgstr "Password" msgstr "Password"
#: lib/mv_web/live/user_live/form.ex:53 #: lib/mv_web/live/user_live/form.ex:85
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Password requirements" msgid "Password requirements"
msgstr "Password requirements" msgstr "Password requirements"
@ -545,56 +494,56 @@ msgstr ""
msgid "Select user" msgid "Select user"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/form.ex:27 #: lib/mv_web/live/user_live/form.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Set Password" msgid "Set Password"
msgstr "Set Password" msgstr "Set Password"
#: lib/mv_web/live/user_live/form.ex:83 #: lib/mv_web/live/user_live/form.ex:115
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "User will be created without a password. Check 'Set Password' to add one." msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "User will be created without a password. Check 'Set Password' to add one." msgstr "User will be created without a password. Check 'Set Password' to add one."
#: lib/mv_web/live/user_live/show.ex:30 #: lib/mv_web/live/user_live/show.ex:55
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Linked Member" msgid "Linked Member"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:41 #: lib/mv_web/live/member_live/show.ex:63
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Linked User" msgid "Linked User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:40 #: lib/mv_web/live/user_live/show.ex:65
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No member linked" msgid "No member linked"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:51 #: lib/mv_web/live/member_live/show.ex:73
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No user linked" msgid "No user linked"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:14 #: lib/mv_web/live/member_live/show.ex:36
#: lib/mv_web/live/member_live/show.ex:16 #: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Back to members list" msgid "Back to members list"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:13 #: lib/mv_web/live/user_live/show.ex:38
#: lib/mv_web/live/user_live/show.ex:15 #: lib/mv_web/live/user_live/show.ex:40
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Back to users list" msgid "Back to users list"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:26 #: lib/mv_web/components/layouts/navbar.ex:27
#: lib/mv_web/components/layouts/navbar.ex:32 #: lib/mv_web/components/layouts/navbar.ex:33
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Select language" msgid "Select language"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:39 #: lib/mv_web/components/layouts/navbar.ex:40
#: lib/mv_web/components/layouts/navbar.ex:59 #: lib/mv_web/components/layouts/navbar.ex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Toggle dark mode" msgid "Toggle dark mode"
msgstr "" msgstr ""
@ -605,7 +554,7 @@ msgstr ""
msgid "Search..." msgid "Search..."
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:20 #: lib/mv_web/components/layouts/navbar.ex:21
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Users" msgid "Users"
msgstr "" msgstr ""
@ -651,3 +600,59 @@ msgstr ""
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account." msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
msgstr "" msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:53
#, elixir-autogen, elixir-format
msgid "Choose a custom field"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:59
#: lib/mv_web/live/member_live/show.ex:78
#, elixir-autogen, elixir-format
msgid "Custom Field Values"
msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:51
#, elixir-autogen, elixir-format
msgid "Custom field"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:114
#, elixir-autogen, elixir-format
msgid "Custom field %{action} successfully"
msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:242
#, elixir-autogen, elixir-format
msgid "Custom field value %{action} successfully"
msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:70
#, elixir-autogen, elixir-format
msgid "Please select a custom field first"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:64
#, elixir-autogen, elixir-format
msgid "Save Custom field"
msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:75
#, elixir-autogen, elixir-format
msgid "Save Custom field value"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:45
#, 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 ""

View file

@ -0,0 +1,19 @@
defmodule Mv.Repo.Migrations.RenamePropertiesToCustomFieldsExtensions1 do
@moduledoc """
Installs any extensions that are mentioned in the repo's `installed_extensions/0` callback
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
execute("CREATE EXTENSION IF NOT EXISTS \"pg_trgm\"")
end
def down do
# Uncomment this if you actually want to uninstall the extensions
# when this migration is rolled back:
# execute("DROP EXTENSION IF EXISTS \"pg_trgm\"")
end
end

View file

@ -0,0 +1,84 @@
defmodule Mv.Repo.Migrations.RenamePropertiesToCustomFields do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
# Rename tables
rename table("property_types"), to: table("custom_fields")
rename table("properties"), to: table("custom_field_values")
# Rename the foreign key column
rename table("custom_field_values"), :property_type_id, to: :custom_field_id
# Drop old foreign key constraints
drop constraint(:custom_field_values, "properties_member_id_fkey")
drop constraint(:custom_field_values, "properties_property_type_id_fkey")
# Add new foreign key constraints with correct names and on_delete behavior
alter table(:custom_field_values) do
modify :member_id,
references(:members,
column: :id,
name: "custom_field_values_member_id_fkey",
type: :uuid,
prefix: "public",
on_delete: :delete_all
)
modify :custom_field_id,
references(:custom_fields,
column: :id,
name: "custom_field_values_custom_field_id_fkey",
type: :uuid,
prefix: "public"
)
end
# Rename indexes
execute "ALTER INDEX IF EXISTS property_types_unique_name_index RENAME TO custom_fields_unique_name_index"
execute "ALTER INDEX IF EXISTS properties_unique_property_per_member_index RENAME TO custom_field_values_unique_custom_field_per_member_index"
end
def down do
# Rename indexes back
execute "ALTER INDEX IF EXISTS custom_fields_unique_name_index RENAME TO property_types_unique_name_index"
execute "ALTER INDEX IF EXISTS custom_field_values_unique_custom_field_per_member_index RENAME TO properties_unique_property_per_member_index"
# Drop new foreign key constraints
drop constraint(:custom_field_values, "custom_field_values_member_id_fkey")
drop constraint(:custom_field_values, "custom_field_values_custom_field_id_fkey")
# Add back old foreign key constraints
alter table(:custom_field_values) do
modify :member_id,
references(:members,
column: :id,
name: "properties_member_id_fkey",
type: :uuid,
prefix: "public"
)
modify :custom_field_id,
references(:custom_fields,
column: :id,
name: "properties_property_type_id_fkey",
type: :uuid,
prefix: "public"
)
end
# Rename the foreign key column back
rename table("custom_field_values"), :custom_field_id, to: :property_type_id
# Rename tables back
rename table("custom_fields"), to: table("property_types")
rename table("custom_field_values"), to: table("properties")
end
end

View file

@ -7,36 +7,94 @@ alias Mv.Membership
alias Mv.Accounts alias Mv.Accounts
for attrs <- [ for attrs <- [
# Basic example fields (for testing)
%{ %{
name: "String Field", name: "String Field",
value_type: :string, value_type: :string,
description: "Example for a field of type string", description: "Example for a field of type string",
immutable: true, immutable: true,
required: true required: false
}, },
%{ %{
name: "Date Field", name: "Date Field",
value_type: :date, value_type: :date,
description: "Example for a field of type date", description: "Example for a field of type date",
immutable: true, immutable: true,
required: true required: false
}, },
%{ %{
name: "Boolean Field", name: "Boolean Field",
value_type: :boolean, value_type: :boolean,
description: "Example for a field of type boolean", description: "Example for a field of type boolean",
immutable: true, immutable: true,
required: true required: false
}, },
%{ %{
name: "Email Field", name: "Email Field",
value_type: :email, value_type: :email,
description: "Example for a field of type email", description: "Example for a field of type email",
immutable: true, immutable: true,
required: true required: false
},
# Realistic custom fields
%{
name: "Membership Number",
value_type: :string,
description: "Unique membership identification number",
immutable: false,
required: false
},
%{
name: "Emergency Contact",
value_type: :string,
description: "Emergency contact person name and phone",
immutable: false,
required: false
},
%{
name: "T-Shirt Size",
value_type: :string,
description: "T-Shirt size for events (XS, S, M, L, XL, XXL)",
immutable: false,
required: false
},
%{
name: "Newsletter Subscription",
value_type: :boolean,
description: "Whether member wants to receive newsletter",
immutable: false,
required: false
},
%{
name: "Date of Last Medical Check",
value_type: :date,
description: "Date of last medical examination",
immutable: false,
required: false
},
%{
name: "Secondary Email",
value_type: :email,
description: "Alternative email address",
immutable: false,
required: false
},
%{
name: "Membership Type",
value_type: :string,
description: "Type of membership (e.g., Regular, Student, Senior)",
immutable: false,
required: false
},
%{
name: "Parking Permit",
value_type: :boolean,
description: "Whether member has parking permit",
immutable: false,
required: false
} }
] do ] do
Membership.create_property_type!( Membership.create_custom_field!(
attrs, attrs,
upsert?: true, upsert?: true,
upsert_identity: :unique_name upsert_identity: :unique_name
@ -180,9 +238,94 @@ Enum.each(linked_members, fn member_attrs ->
end end
end) end)
# Create sample custom field values for some members
all_members = Ash.read!(Membership.Member)
all_custom_fields = Ash.read!(Membership.CustomField)
# Helper function to find custom field by name
find_field = fn name -> Enum.find(all_custom_fields, &(&1.name == name)) end
find_member = fn email -> Enum.find(all_members, &(&1.email == email)) end
# Add custom field values for Hans Müller
if hans = find_member.("hans.mueller@example.de") do
[
{find_field.("Membership Number"),
%{"_union_type" => "string", "_union_value" => "M-2023-001"}},
{find_field.("T-Shirt Size"), %{"_union_type" => "string", "_union_value" => "L"}},
{find_field.("Newsletter Subscription"),
%{"_union_type" => "boolean", "_union_value" => true}},
{find_field.("Membership Type"), %{"_union_type" => "string", "_union_value" => "Regular"}},
{find_field.("Parking Permit"), %{"_union_type" => "boolean", "_union_value" => true}},
{find_field.("Secondary Email"),
%{"_union_type" => "email", "_union_value" => "hans.m@private.de"}}
]
|> Enum.each(fn {field, value} ->
if field do
Membership.CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: hans.id,
custom_field_id: field.id,
value: value
})
|> Ash.create!(upsert?: true, upsert_identity: :unique_custom_field_per_member)
end
end)
end
# Add custom field values for Greta Schmidt
if greta = find_member.("greta.schmidt@example.de") do
[
{find_field.("Membership Number"),
%{"_union_type" => "string", "_union_value" => "M-2023-015"}},
{find_field.("T-Shirt Size"), %{"_union_type" => "string", "_union_value" => "M"}},
{find_field.("Newsletter Subscription"),
%{"_union_type" => "boolean", "_union_value" => true}},
{find_field.("Membership Type"), %{"_union_type" => "string", "_union_value" => "Student"}},
{find_field.("Emergency Contact"),
%{"_union_type" => "string", "_union_value" => "Anna Schmidt, +49301234567"}}
]
|> Enum.each(fn {field, value} ->
if field do
Membership.CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: greta.id,
custom_field_id: field.id,
value: value
})
|> Ash.create!(upsert?: true, upsert_identity: :unique_custom_field_per_member)
end
end)
end
# Add custom field values for Friedrich Wagner
if friedrich = find_member.("friedrich.wagner@example.de") do
[
{find_field.("Membership Number"),
%{"_union_type" => "string", "_union_value" => "M-2022-042"}},
{find_field.("T-Shirt Size"), %{"_union_type" => "string", "_union_value" => "XL"}},
{find_field.("Newsletter Subscription"),
%{"_union_type" => "boolean", "_union_value" => false}},
{find_field.("Membership Type"), %{"_union_type" => "string", "_union_value" => "Senior"}},
{find_field.("Parking Permit"), %{"_union_type" => "boolean", "_union_value" => false}},
{find_field.("Date of Last Medical Check"),
%{"_union_type" => "date", "_union_value" => ~D[2024-03-15]}}
]
|> Enum.each(fn {field, value} ->
if field do
Membership.CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: friedrich.id,
custom_field_id: field.id,
value: value
})
|> Ash.create!(upsert?: true, upsert_identity: :unique_custom_field_per_member)
end
end)
end
IO.puts("✅ Seeds completed successfully!") IO.puts("✅ Seeds completed successfully!")
IO.puts("📝 Created sample data:") IO.puts("📝 Created sample data:")
IO.puts(" - Property types: String, Date, Boolean, Email") IO.puts(" - Custom fields: 12 fields (String, Date, Boolean, Email, + 8 realistic fields)")
IO.puts(" - Admin user: admin@mv.local (password: testpassword)") IO.puts(" - Admin user: admin@mv.local (password: testpassword)")
IO.puts(" - Sample members: Hans, Greta, Friedrich") IO.puts(" - Sample members: Hans, Greta, Friedrich")
@ -194,4 +337,8 @@ IO.puts(
" - Linked members: Maria Weber ↔ maria.weber@example.de, Thomas Klein ↔ thomas.klein@example.de" " - Linked members: Maria Weber ↔ maria.weber@example.de, Thomas Klein ↔ thomas.klein@example.de"
) )
IO.puts("🔗 Visit the application to see user-member relationships in action!") IO.puts(
" - Custom field values: Sample data for Hans (6 fields), Greta (5 fields), Friedrich (6 fields)"
)
IO.puts("🔗 Visit the application to see user-member relationships and custom fields in action!")

View file

@ -0,0 +1,124 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": null,
"scale": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "value",
"type": "map"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "custom_field_values_member_id_fkey",
"on_delete": "delete",
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "members"
},
"scale": null,
"size": null,
"source": "member_id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "custom_field_values_custom_field_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "custom_fields"
},
"scale": null,
"size": null,
"source": "custom_field_id",
"type": "uuid"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "DFA12C7D80B09C2EE5125469A1EDEF0412C7B2A7E44A9FD97A1387C52C8D7753",
"identities": [
{
"all_tenants?": false,
"base_filter": null,
"index_name": "custom_field_values_unique_custom_field_per_member_index",
"keys": [
{
"type": "atom",
"value": "member_id"
},
{
"type": "atom",
"value": "custom_field_id"
}
],
"name": "unique_custom_field_per_member",
"nils_distinct?": true,
"where": null
}
],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.Mv.Repo",
"schema": null,
"table": "custom_field_values"
}

View file

@ -0,0 +1,106 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": null,
"scale": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "name",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "value_type",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "description",
"type": "text"
},
{
"allow_nil?": false,
"default": "false",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "immutable",
"type": "boolean"
},
{
"allow_nil?": false,
"default": "false",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "required",
"type": "boolean"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "B98535258034AE3C37FCB7AF054B97D7CCADE3CA7015B1B93C64CDE1250807EE",
"identities": [
{
"all_tenants?": false,
"base_filter": null,
"index_name": "custom_fields_unique_name_index",
"keys": [
{
"type": "atom",
"value": "name"
}
],
"name": "unique_name",
"nils_distinct?": true,
"where": null
}
],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.Mv.Repo",
"schema": null,
"table": "custom_fields"
}

View file

@ -2,6 +2,7 @@
"ash_functions_version": 5, "ash_functions_version": 5,
"installed": [ "installed": [
"ash-functions", "ash-functions",
"citext" "citext",
"pg_trgm"
] ]
} }

View file

@ -0,0 +1,205 @@
defmodule Mv.Membership.CustomFieldValidationTest do
@moduledoc """
Tests for CustomField validation constraints.
Tests cover:
- Name length validation (max 100 characters)
- Name trimming
- Description length validation (max 500 characters)
- Description trimming
- Required vs optional fields
"""
use Mv.DataCase, async: true
alias Mv.Membership.CustomField
describe "name validation" do
test "accepts name with exactly 100 characters" do
name = String.duplicate("a", 100)
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: name,
value_type: :string
})
|> Ash.create()
assert custom_field.name == name
assert String.length(custom_field.name) == 100
end
test "rejects name with 101 characters" do
name = String.duplicate("a", 101)
assert {:error, changeset} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: name,
value_type: :string
})
|> Ash.create()
assert [%{field: :name, message: message}] = changeset.errors
assert message =~ "max" or message =~ "length" or message =~ "100"
end
test "trims whitespace from name" do
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: " test_field ",
value_type: :string
})
|> Ash.create()
assert custom_field.name == "test_field"
end
test "rejects empty name" do
assert {:error, changeset} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "",
value_type: :string
})
|> Ash.create()
assert Enum.any?(changeset.errors, fn error -> error.field == :name end)
end
test "rejects nil name" do
assert {:error, changeset} =
CustomField
|> Ash.Changeset.for_create(:create, %{
value_type: :string
})
|> Ash.create()
assert Enum.any?(changeset.errors, fn error -> error.field == :name end)
end
end
describe "description validation" do
test "accepts description with exactly 500 characters" do
description = String.duplicate("a", 500)
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field",
value_type: :string,
description: description
})
|> Ash.create()
assert custom_field.description == description
assert String.length(custom_field.description) == 500
end
test "rejects description with 501 characters" do
description = String.duplicate("a", 501)
assert {:error, changeset} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field",
value_type: :string,
description: description
})
|> Ash.create()
assert [%{field: :description, message: message}] = changeset.errors
assert message =~ "max" or message =~ "length" or message =~ "500"
end
test "trims whitespace from description" do
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field",
value_type: :string,
description: " A nice description "
})
|> Ash.create()
assert custom_field.description == "A nice description"
end
test "accepts nil description (optional field)" do
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field",
value_type: :string
})
|> Ash.create()
assert custom_field.description == nil
end
test "accepts empty description after trimming" do
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field",
value_type: :string,
description: " "
})
|> Ash.create()
# After trimming whitespace, becomes nil (empty strings are converted to nil)
assert custom_field.description == nil
end
end
describe "name uniqueness" do
test "rejects duplicate names" do
assert {:ok, _} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "unique_field",
value_type: :string
})
|> Ash.create()
assert {:error, changeset} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "unique_field",
value_type: :integer
})
|> Ash.create()
assert Enum.any?(changeset.errors, fn error -> error.field == :name end)
end
end
describe "value_type validation" do
test "accepts all valid value types" do
for value_type <- [:string, :integer, :boolean, :date, :email] do
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "field_#{value_type}",
value_type: value_type
})
|> Ash.create()
assert custom_field.value_type == value_type
end
end
test "rejects invalid value type" do
assert {:error, changeset} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "invalid_field",
value_type: :invalid_type
})
|> Ash.create()
assert [%{field: :value_type}] = changeset.errors
end
end
end

View file

@ -0,0 +1,305 @@
defmodule Mv.Membership.CustomFieldValueValidationTest do
@moduledoc """
Tests for CustomFieldValue validation constraints.
Tests cover:
- String value length validation (max 10,000 characters)
- String value trimming
- Email value validation (via Email type)
- Optional values (nil allowed)
"""
use Mv.DataCase, async: true
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
setup do
# Create a test member
{:ok, member} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "Test",
last_name: "User",
email: "test.validation@example.com"
})
|> Ash.create()
# Create custom fields for different types
{:ok, string_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "string_field",
value_type: :string
})
|> Ash.create()
{:ok, integer_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "integer_field",
value_type: :integer
})
|> Ash.create()
{:ok, email_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "email_field",
value_type: :email
})
|> Ash.create()
%{
member: member,
string_field: string_field,
integer_field: integer_field,
email_field: email_field
}
end
describe "string value length validation" do
test "accepts string value with exactly 10,000 characters", %{
member: member,
string_field: string_field
} do
value_string = String.duplicate("a", 10_000)
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: string_field.id,
value: %{
"_union_type" => "string",
"_union_value" => value_string
}
})
|> Ash.create()
assert custom_field_value.value.value == value_string
assert String.length(custom_field_value.value.value) == 10_000
end
test "rejects string value with 10,001 characters", %{
member: member,
string_field: string_field
} do
value_string = String.duplicate("a", 10_001)
assert {:error, changeset} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: string_field.id,
value: %{"_union_type" => "string", "_union_value" => value_string}
})
|> Ash.create()
assert Enum.any?(changeset.errors, fn error ->
error.field == :value and (error.message =~ "max" or error.message =~ "length")
end)
end
test "trims whitespace from string value", %{member: member, string_field: string_field} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: string_field.id,
value: %{"_union_type" => "string", "_union_value" => " test value "}
})
|> Ash.create()
assert custom_field_value.value.value == "test value"
end
test "accepts empty string value", %{member: member, string_field: string_field} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: string_field.id,
value: %{"_union_type" => "string", "_union_value" => ""}
})
|> Ash.create()
# Empty strings after trimming become nil
assert custom_field_value.value.value == nil
end
test "accepts string with special characters", %{member: member, string_field: string_field} do
special_string = "Hello 世界! 🎉 @#$%^&*()"
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: string_field.id,
value: %{"_union_type" => "string", "_union_value" => special_string}
})
|> Ash.create()
assert custom_field_value.value.value == special_string
end
end
describe "integer value validation" do
test "accepts valid integer value", %{member: member, integer_field: integer_field} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: integer_field.id,
value: %{"_union_type" => "integer", "_union_value" => 42}
})
|> Ash.create()
assert custom_field_value.value.value == 42
end
test "accepts negative integer", %{member: member, integer_field: integer_field} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: integer_field.id,
value: %{"_union_type" => "integer", "_union_value" => -100}
})
|> Ash.create()
assert custom_field_value.value.value == -100
end
test "accepts zero", %{member: member, integer_field: integer_field} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: integer_field.id,
value: %{"_union_type" => "integer", "_union_value" => 0}
})
|> Ash.create()
assert custom_field_value.value.value == 0
end
end
describe "email value validation" do
test "accepts nil value (optional field)", %{member: member, email_field: email_field} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: email_field.id,
value: %{"_union_type" => "email", "_union_value" => nil}
})
|> Ash.create()
assert custom_field_value.value.value == nil
end
test "accepts empty string (becomes nil after trim)", %{
member: member,
email_field: email_field
} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: email_field.id,
value: %{"_union_type" => "email", "_union_value" => ""}
})
|> Ash.create()
# Empty string after trim should become nil
assert custom_field_value.value.value == nil
end
test "accepts valid email", %{member: member, email_field: email_field} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: email_field.id,
value: %{"_union_type" => "email", "_union_value" => "test@example.com"}
})
|> Ash.create()
assert custom_field_value.value.value == "test@example.com"
end
test "rejects invalid email format", %{member: member, email_field: email_field} do
assert {:error, changeset} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: email_field.id,
value: %{"_union_type" => "email", "_union_value" => "not-an-email"}
})
|> Ash.create()
assert Enum.any?(changeset.errors, fn error -> error.field == :value end)
end
test "rejects email longer than 254 characters", %{member: member, email_field: email_field} do
# Create an email with >254 chars (243 + 12 = 255)
long_email = String.duplicate("a", 243) <> "@example.com"
assert {:error, changeset} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: email_field.id,
value: %{"_union_type" => "email", "_union_value" => long_email}
})
|> Ash.create()
assert Enum.any?(changeset.errors, fn error -> error.field == :value end)
end
test "trims whitespace from email", %{member: member, email_field: email_field} do
assert {:ok, custom_field_value} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: email_field.id,
value: %{"_union_type" => "email", "_union_value" => " test@example.com "}
})
|> Ash.create()
assert custom_field_value.value.value == "test@example.com"
end
end
describe "uniqueness constraint" do
test "rejects duplicate custom_field_id per member", %{
member: member,
string_field: string_field
} do
# Create first custom field value
assert {:ok, _} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: string_field.id,
value: %{"_union_type" => "string", "_union_value" => "first value"}
})
|> Ash.create()
# Try to create second custom field value with same custom_field_id for same member
assert {:error, changeset} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: string_field.id,
value: %{"_union_type" => "string", "_union_value" => "second value"}
})
|> Ash.create()
# Should have uniqueness error
assert Enum.any?(changeset.errors, fn error ->
error.message =~ "unique" or error.message =~ "already exists" or
error.message =~ "has already been taken"
end)
end
end
end

View file

@ -148,10 +148,10 @@ defmodule MvWeb.ProfileNavigationTest do
"/", "/",
"/members", "/members",
"/members/new", "/members/new",
"/properties", "/custom_field_values",
"/properties/new", "/custom_field_values/new",
"/property_types", "/custom_fields",
"/property_types/new", "/custom_fields/new",
"/users", "/users",
"/users/new" "/users/new"
] ]

View file

@ -9,11 +9,11 @@ defmodule Mv.SeedsTest do
# Basic smoke test: ensure some data was created # Basic smoke test: ensure some data was created
{:ok, users} = Ash.read(Mv.Accounts.User) {:ok, users} = Ash.read(Mv.Accounts.User)
{:ok, members} = Ash.read(Mv.Membership.Member) {:ok, members} = Ash.read(Mv.Membership.Member)
{:ok, property_types} = Ash.read(Mv.Membership.PropertyType) {:ok, custom_fields} = Ash.read(Mv.Membership.CustomField)
assert length(users) > 0, "Seeds should create at least one user" assert length(users) > 0, "Seeds should create at least one user"
assert length(members) > 0, "Seeds should create at least one member" assert length(members) > 0, "Seeds should create at least one member"
assert length(property_types) > 0, "Seeds should create at least one property type" assert length(custom_fields) > 0, "Seeds should create at least one custom field"
end end
test "can be run multiple times (idempotent)" do test "can be run multiple times (idempotent)" do
@ -23,7 +23,7 @@ defmodule Mv.SeedsTest do
# Count records # Count records
{:ok, users_count_1} = Ash.read(Mv.Accounts.User) {:ok, users_count_1} = Ash.read(Mv.Accounts.User)
{:ok, members_count_1} = Ash.read(Mv.Membership.Member) {:ok, members_count_1} = Ash.read(Mv.Membership.Member)
{:ok, property_types_count_1} = Ash.read(Mv.Membership.PropertyType) {:ok, custom_fields_count_1} = Ash.read(Mv.Membership.CustomField)
# Run seeds second time - should not raise errors # Run seeds second time - should not raise errors
assert Code.eval_file("priv/repo/seeds.exs") assert Code.eval_file("priv/repo/seeds.exs")
@ -31,7 +31,7 @@ defmodule Mv.SeedsTest do
# Count records again - should be the same (upsert, not duplicate) # Count records again - should be the same (upsert, not duplicate)
{:ok, users_count_2} = Ash.read(Mv.Accounts.User) {:ok, users_count_2} = Ash.read(Mv.Accounts.User)
{:ok, members_count_2} = Ash.read(Mv.Membership.Member) {:ok, members_count_2} = Ash.read(Mv.Membership.Member)
{:ok, property_types_count_2} = Ash.read(Mv.Membership.PropertyType) {:ok, custom_fields_count_2} = Ash.read(Mv.Membership.CustomField)
assert length(users_count_1) == length(users_count_2), assert length(users_count_1) == length(users_count_2),
"Users count should remain same after re-running seeds" "Users count should remain same after re-running seeds"
@ -39,8 +39,8 @@ defmodule Mv.SeedsTest do
assert length(members_count_1) == length(members_count_2), assert length(members_count_1) == length(members_count_2),
"Members count should remain same after re-running seeds" "Members count should remain same after re-running seeds"
assert length(property_types_count_1) == length(property_types_count_2), assert length(custom_fields_count_1) == length(custom_fields_count_2),
"PropertyTypes count should remain same after re-running seeds" "CustomFields count should remain same after re-running seeds"
end end
end end
end end