diff --git a/CODE_GUIDELINES.md b/CODE_GUIDELINES.md
index 4a82edb..5cc792c 100644
--- a/CODE_GUIDELINES.md
+++ b/CODE_GUIDELINES.md
@@ -81,8 +81,8 @@ lib/
├── membership/ # Membership domain
│ ├── membership.ex # Domain definition
│ ├── member.ex # Member resource
-│ ├── property.ex # Custom property resource
-│ ├── property_type.ex # Property type resource
+│ ├── custom_field_value.ex # Custom field value resource
+│ ├── custom_field.ex # CustomFieldValue type resource
│ └── email.ex # Email custom type
├── mv/ # Core application modules
│ ├── accounts/ # Domain-specific logic
@@ -121,8 +121,8 @@ lib/
│ │ │ ├── search_bar_component.ex
│ │ │ └── sort_header_component.ex
│ │ ├── member_live/ # Member CRUD LiveViews
-│ │ ├── property_live/ # Property CRUD LiveViews
-│ │ ├── property_type_live/
+│ │ ├── custom_field_value_live/ # CustomFieldValue CRUD LiveViews
+│ │ ├── custom_field_live/
│ │ └── user_live/ # User management LiveViews
│ ├── auth_overrides.ex # AshAuthentication overrides
│ ├── endpoint.ex # Phoenix endpoint
@@ -740,14 +740,14 @@ end
# Good - preload relationships
members =
Member
- |> Ash.Query.load(:properties)
+ |> Ash.Query.load(:custom_field_values)
|> Mv.Membership.list_members!()
# Avoid - causes N+1 queries
members = Mv.Membership.list_members!()
Enum.map(members, fn member ->
# This triggers a query for each member
- Ash.load!(member, :properties)
+ Ash.load!(member, :custom_field_values)
end)
```
@@ -1723,13 +1723,13 @@ end
# Good - preload relationships
members =
Member
- |> Ash.Query.load([:properties, :user])
+ |> Ash.Query.load([:custom_field_values, :user])
|> Mv.Membership.list_members!()
# Avoid - causes N+1
members = Mv.Membership.list_members!()
Enum.map(members, fn member ->
- properties = Ash.load!(member, :properties) # N queries!
+ custom_field_values = Ash.load!(member, :custom_field_values) # N queries!
end)
```
@@ -1904,7 +1904,7 @@ defmodule Mv.Membership.Member do
@moduledoc """
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.
## Examples
@@ -2050,7 +2050,7 @@ open doc/index.html
## [Unreleased]
### Added
-- Member custom properties feature
+- Member custom_field_values feature
- Email synchronization between user and member
### Changed
@@ -2081,14 +2081,14 @@ open doc/index.html
```bash
# Create feature branch
-git checkout -b feature/member-custom-properties
+git checkout -b feature/member-custom-custom_field_values
# Work on feature
git add .
-git commit -m "Add custom properties to members"
+git commit -m "Add custom_field_values to members"
# Push to remote
-git push origin feature/member-custom-properties
+git push origin feature/member-custom-custom_field_values
```
### 8.2 Commit Messages
@@ -2127,7 +2127,7 @@ Closes #123
```
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.
```
diff --git a/docs/database-schema-readme.md b/docs/database-schema-readme.md
index eefb608..d548b82 100644
--- a/docs/database-schema-readme.md
+++ b/docs/database-schema-readme.md
@@ -52,21 +52,21 @@ This document provides a comprehensive overview of the Mila Membership Managemen
- Bidirectional email sync with users
- Flexible address and contact data
-#### `properties`
+#### `custom_field_values`
- **Purpose:** Dynamic custom member attributes
- **Rows (Estimated):** Variable (N per member)
- **Key Features:**
- Union type value storage (JSONB)
- Multiple data types supported
- - One property per type per member
+ - One custom field value per custom field per member
-#### `property_types`
-- **Purpose:** Schema definitions for custom properties
+#### `custom_fields`
+- **Purpose:** Schema definitions for custom_field_values
- **Rows (Estimated):** Low (admin-defined)
- **Key Features:**
- Type definitions
- Immutable and required flags
- - Centralized property management
+ - Centralized custom field management
## Key Relationships
@@ -77,7 +77,7 @@ User (0..1) ←→ (0..1) Member
Member (1) → (N) Properties
↓
- PropertyType (1)
+ CustomField (1)
```
### Relationship Details
@@ -90,11 +90,11 @@ Member (1) → (N) Properties
- `ON DELETE SET NULL` on user side (User preserved when Member deleted)
2. **Member → Properties (1:N)**
- - One member, many properties
- - `ON DELETE CASCADE` - properties deleted with member
- - Composite unique constraint (member_id, property_type_id)
+ - One member, many custom_field_values
+ - `ON DELETE CASCADE` - custom_field_values deleted with member
+ - Composite unique constraint (member_id, custom_field_id)
-3. **Property → PropertyType (N:1)**
+3. **CustomFieldValue → CustomField (N:1)**
- Properties reference type definition
- `ON DELETE RESTRICT` - cannot delete type if in use
- Type defines data structure
@@ -121,8 +121,8 @@ Member (1) → (N) Properties
- Phone: `+?[0-9\- ]{6,20}`
- Postal code: 5 digits
-### Property System
-- Maximum one property per type per member
+### CustomFieldValue System
+- Maximum one custom field value per custom field per member
- Value stored as union type in JSONB
- Supported types: string, integer, boolean, date, email
- Types can be marked as immutable or required
@@ -144,10 +144,10 @@ Member (1) → (N) Properties
- `join_date` (B-tree) - Date filtering
- `paid` (partial B-tree) - Payment status queries
-**properties:**
-- `member_id` - Member property lookups
-- `property_type_id` - Type-based queries
-- Composite `(member_id, property_type_id)` - Uniqueness
+**custom_field_values:**
+- `member_id` - Member custom field value lookups
+- `custom_field_id` - Type-based queries
+- Composite `(member_id, custom_field_id)` - Uniqueness
**tokens:**
- `subject` - User token lookups
@@ -297,8 +297,8 @@ priv/repo/migrations/
| Relationship | On Delete | Rationale |
|--------------|-----------|-----------|
| `users.member_id → members.id` | SET NULL | Preserve user account when member deleted |
-| `properties.member_id → members.id` | CASCADE | Delete properties with member |
-| `properties.property_type_id → property_types.id` | RESTRICT | Prevent deletion of types in use |
+| `custom_field_values.member_id → members.id` | CASCADE | Delete custom_field_values with member |
+| `custom_field_values.custom_field_id → custom_fields.id` | RESTRICT | Prevent deletion of types in use |
### Validation Layers
@@ -327,15 +327,15 @@ priv/repo/migrations/
- Member search (uses GIN index on search_vector)
- Member list with filters (uses indexes on join_date, paid)
- 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:**
- Member CRUD operations
-- Property updates
+- CustomFieldValue updates
- Token validation
**Low Frequency:**
-- PropertyType management
+- CustomField management
- User-Member linking
- Bulk operations
@@ -396,10 +396,10 @@ Install "DBML Language" extension to view/edit DBML files with:
### Critical Tables (Priority 1)
- `members` - Core business data
- `users` - Authentication data
-- `property_types` - Schema definitions
+- `custom_fields` - Schema definitions
### Important Tables (Priority 2)
-- `properties` - Member custom data
+- `custom_field_values` - Member custom data
- `tokens` - Can be regenerated but good to backup
### Backup Strategy
diff --git a/docs/database_schema.dbml b/docs/database_schema.dbml
index b414cf9..431e064 100644
--- a/docs/database_schema.dbml
+++ b/docs/database_schema.dbml
@@ -18,7 +18,7 @@ Project mila_membership_management {
## Key Features:
- 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
- Full-text search capabilities (tsvector)
- Fuzzy search with trigram matching (pg_trgm)
@@ -26,7 +26,7 @@ Project mila_membership_management {
## Domains:
- **Accounts**: User authentication and session management
- - **Membership**: Club member data and custom properties
+ - **Membership**: Club member data and custom fields
## Required PostgreSQL Extensions:
- uuid-ossp (UUID generation)
@@ -178,7 +178,7 @@ Table members {
**Relationships:**
- 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:**
- 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']
value jsonb [null, note: 'Union type value storage (format: {type: "string", value: "example"})']
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 {
- (member_id, property_type_id) [unique, name: 'properties_unique_property_per_member_index', note: 'One property per type per member']
- member_id [name: 'properties_member_id_idx']
- property_type_id [name: 'properties_property_type_id_idx']
+ (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: 'custom_field_values_member_id_idx']
+ custom_field_id [name: 'custom_field_values_custom_field_id_idx']
}
Note: '''
- **Dynamic Custom Member Properties**
+ **Dynamic Custom Member Field Values**
Provides flexible, extensible attributes for members beyond the fixed schema.
@@ -221,9 +221,9 @@ Table properties {
- `email`: Validated email addresses
**Constraints:**
- - Each member can have only ONE property per property_type
- - Properties are deleted when member is deleted (CASCADE)
- - Property type cannot be deleted if properties exist (RESTRICT)
+ - Each member can have only ONE custom field value per custom field
+ - Custom field values are deleted when member is deleted (CASCADE)
+ - Custom field cannot be deleted if custom field values exist (RESTRICT)
**Use Cases:**
- 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']
- 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']
description text [null, note: 'Human-readable description']
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 {
- name [unique, name: 'property_types_unique_name_index']
+ name [unique, name: 'custom_fields_unique_name_index']
}
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:**
- - `name`: Unique identifier for the property type
+ - `name`: Unique identifier for the custom field
- `value_type`: Enforces data type consistency
- `description`: Documentation for users/admins
- `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:**
- `value_type` must be one of: string, integer, boolean, date, email
- - `name` must be unique across all property types
- - Cannot be deleted if properties reference it (ON DELETE RESTRICT)
+ - `name` must be unique across all custom fields
+ - Cannot be deleted if custom_field_values reference it (ON DELETE RESTRICT)
**Examples:**
- Membership Number (string, immutable, required)
@@ -283,25 +283,25 @@ Table property_types {
Ref: users.member_id - members.id [delete: set null]
// Member → Properties (1:N)
-// - One member can have multiple properties
-// - Each property belongs to exactly one member
+// - One member can have multiple custom_field_values
+// - Each custom field value belongs to exactly one member
// - ON DELETE CASCADE: Properties deleted when member deleted
-// - UNIQUE constraint: One property per type per member
-Ref: properties.member_id > members.id [delete: cascade]
+// - UNIQUE constraint: One custom field value per custom field per member
+Ref: custom_field_values.member_id > members.id [delete: cascade]
-// Property → PropertyType (N:1)
-// - Many properties can reference one property type
-// - Property type defines the schema/behavior
-// - ON DELETE RESTRICT: Cannot delete type if properties exist
-Ref: properties.property_type_id > property_types.id [delete: restrict]
+// CustomFieldValue → CustomField (N:1)
+// - Many custom_field_values can reference one custom field
+// - CustomFieldValue type defines the schema/behavior
+// - ON DELETE RESTRICT: Cannot delete type if custom_field_values exist
+Ref: custom_field_values.custom_field_id > custom_fields.id [delete: restrict]
// ============================================
// ENUMS
// ============================================
-// Valid data types for property values
-// Determines how Property.value is interpreted
-Enum property_value_type {
+// Valid data types for custom field values
+// Determines how CustomFieldValue.value is interpreted
+Enum custom_field_value_type {
string [note: 'Text data']
integer [note: 'Numeric data']
boolean [note: 'True/False flags']
@@ -335,8 +335,8 @@ TableGroup accounts_domain {
TableGroup membership_domain {
members
- properties
- property_types
+ custom_field_values
+ custom_fields
Note: '''
**Membership Domain**
diff --git a/docs/development-progress-log.md b/docs/development-progress-log.md
index 0022631..f7447f2 100644
--- a/docs/development-progress-log.md
+++ b/docs/development-progress-log.md
@@ -131,11 +131,11 @@ Based on closed PRs from https://git.local-it.org/local-it/mitgliederverwaltung/
**Sprint 3 - 28.05 - 09.07**
- Member CRUD operations
-- Basic property system
+- Basic custom field system
- Initial UI with Tailwind CSS
**Sprint 4 - 09.07 - 30.07**
-- Property types implementation
+- CustomFieldValue types implementation
- Data validation
- 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*
- Comprehensive seed data
- Test users and members
-- Property type examples
+- CustomFieldValue type examples
#### 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.
-#### 4. Property System (EAV Pattern)
+#### 4. CustomFieldValue System (EAV Pattern)
**Implementation:** Entity-Attribute-Value pattern with union types
```elixir
-# Property Type defines schema
-defmodule Mv.Membership.PropertyType do
+# CustomFieldValue Type defines schema
+defmodule Mv.Membership.CustomField do
attribute :name, :string # "Membership Number"
attribute :value_type, :atom # :string, :integer, :boolean, :date, :email
attribute :immutable, :boolean # Can't change after creation
attribute :required, :boolean # All members must have this
end
-# Property stores values
-defmodule Mv.Membership.Property do
+# CustomFieldValue stores values
+defmodule Mv.Membership.CustomFieldValue do
attribute :value, :union, # Polymorphic value storage
constraints: [
types: [
@@ -405,7 +405,7 @@ defmodule Mv.Membership.Property do
]
]
belongs_to :member
- belongs_to :property_type
+ belongs_to :custom_field
end
```
@@ -413,12 +413,12 @@ end
- Clubs need different custom fields
- No schema migrations for new fields
- Type safety with union types
-- Centralized property management
+- Centralized custom field management
**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)
-- Property types protected if in use (RESTRICT)
+- CustomFieldValue types protected if in use (RESTRICT)
#### 5. Authentication Strategy
@@ -593,7 +593,7 @@ end
#### Database Migrations
**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
3. `20250620110850_add_accounts_domain.exs` - Users & tokens tables
4. `20250912085235_AddSearchVectorToMembers.exs` - Full-text search (tsvector + GIN index)
@@ -772,7 +772,7 @@ end
- Admin user: `admin@mv.local` / `testpassword`
- Sample members: Hans Müller, Greta Schmidt, Friedrich Wagner
- Linked accounts: Maria Weber, Thomas Klein
-- Property types: String, Date, Boolean, Email
+- CustomFieldValue types: String, Date, Boolean, Email
**Test Helpers:**
```elixir
@@ -956,9 +956,9 @@ mix credo --strict
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:**
```
@@ -966,16 +966,16 @@ mix credo suggest --format=oneline
```
**Solution:**
-Ensure property value matches property_type.value_type:
+Ensure custom field value matches custom_field.value_type:
```elixir
-# Property Type: value_type = :integer
-property_type = get_property_type("age")
+# CustomFieldValue Type: value_type = :integer
+custom_field = get_custom_field("age")
-# Property Value: must be integer union type
-{:ok, property} = create_property(%{
+# CustomFieldValue Value: must be integer union type
+{:ok, custom_field_value} = create_custom_field_value(%{
value: %{type: :integer, value: 25}, # Not "25" as string
- property_type_id: property_type.id
+ custom_field_id: custom_field.id
})
```
diff --git a/docs/feature-roadmap.md b/docs/feature-roadmap.md
index 5ffd980..9a6517d 100644
--- a/docs/feature-roadmap.md
+++ b/docs/feature-roadmap.md
@@ -87,12 +87,12 @@
---
-#### 3. **Custom Fields (Property System)** 🔧
+#### 3. **Custom Fields (CustomFieldValue System)** 🔧
**Current State:**
-- ✅ Property types (string, integer, boolean, date, email)
-- ✅ Property type management
-- ✅ Dynamic property assignment to members
+- ✅ CustomFieldValue types (string, integer, boolean, date, email)
+- ✅ CustomFieldValue type management
+- ✅ Dynamic custom field value assignment to members
- ✅ Union type storage (JSONB)
**Open Issues:**
@@ -217,7 +217,7 @@
- ❌ Global settings management
- ❌ Club/Organization profile
- ❌ Email templates configuration
-- ❌ Property type management UI (user-facing)
+- ❌ CustomFieldValue type management UI (user-facing)
- ❌ Role and permission management UI
- ❌ System health dashboard
- ❌ 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 |
|-------|---------|------|--------------|--------|
| `/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/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
@@ -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 |
| `link_user` | Link user to member | `%{"user_id" => id}` | 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 |
-| `remove_property` | Remove custom property | `%{"property_id" => id}` | Update form |
+| `add_custom_field_value` | Add custom field value | `%{"custom_field_id" => id, "value" => val}` | Update form |
+| `remove_custom_field_value` | Remove custom field value | `%{"custom_field_value_id" => id}` | Update form |
#### 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` | `:advanced_search` | Multi-criteria search | 🔐 | `{filters: [{field, op, value}]}` | `[%Member{}]` |
| `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_update` | Update multiple members | 🛡️ | `{ids, attrs}` | `{:ok, count}` |
| `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
| Mount | Purpose | Auth | Events |
|-------|---------|------|--------|
-| `/property-types` | List property types | 🛡️ | `new`, `edit`, `delete` |
-| `/property-types/new` | Create property type | 🛡️ | `save`, `cancel` |
-| `/property-types/:id/edit` | Edit property type | 🛡️ | `save`, `cancel`, `delete` |
+| `/custom-fields` | List custom fields | 🛡️ | `new`, `edit`, `delete` |
+| `/custom-fields/new` | Create custom field | 🛡️ | `save`, `cancel` |
+| `/custom-fields/:id/edit` | Edit custom field | 🛡️ | `save`, `cancel`, `delete` |
#### Ash Resource Actions
| Resource | Action | Purpose | Auth | Input | Output |
|----------|--------|---------|------|-------|--------|
-| `PropertyType` | `:create` | Create property type | 🛡️ | `{name, value_type, description, ...}` | `{:ok, property_type}` |
-| `PropertyType` | `:read` | List property types | 🔐 | - | `[%PropertyType{}]` |
-| `PropertyType` | `:update` | Update property type | 🛡️ | `{id, attrs}` | `{:ok, property_type}` |
-| `PropertyType` | `:destroy` | Delete property type | 🛡️ | `{id}` | `{:ok, property_type}` |
-| `Property` | `:create` | Add property to member | 🔐 | `{member_id, property_type_id, value}` | `{:ok, property}` |
-| `Property` | `:update` | Update property value | 🔐 | `{id, value}` | `{:ok, property}` |
-| `Property` | `:destroy` | Remove property | 🔐 | `{id}` | `{:ok, property}` |
+| `CustomField` | `:create` | Create custom field | 🛡️ | `{name, value_type, description, ...}` | `{:ok, custom_field}` |
+| `CustomField` | `:read` | List custom fields | 🔐 | - | `[%CustomField{}]` |
+| `CustomField` | `:update` | Update custom field | 🛡️ | `{id, attrs}` | `{:ok, custom_field}` |
+| `CustomField` | `:destroy` | Delete custom field | 🛡️ | `{id}` | `{:ok, custom_field}` |
+| `CustomFieldValue` | `:create` | Add custom field value to member | 🔐 | `{member_id, custom_field_id, value}` | `{:ok, custom_field_value}` |
+| `CustomFieldValue` | `:update` | Update custom field value | 🔐 | `{id, value}` | `{:ok, custom_field_value}` |
+| `CustomFieldValue` | `:destroy` | Remove custom field value | 🔐 | `{id}` | `{:ok, custom_field_value}` |
#### **NEW: Enhanced Custom Fields** (Issue #194, #157, #161, #153)
| Resource | Action | Purpose | Auth | Input | Output |
|----------|--------|---------|------|-------|--------|
-| `PropertyType` | `:set_default_visibility` | Show/hide by default | 🛡️ | `{id, visible}` | `{:ok, property_type}` |
-| `PropertyType` | `:set_required` | Mark as required | 🛡️ | `{id, required}` | `{:ok, property_type}` |
-| `PropertyType` | `:add_validation` | Add validation rule | 🛡️ | `{id, rule_type, params}` | `{:ok, property_type}` |
-| `PropertyType` | `:create_group` | Create field group | 🛡️ | `{name, property_type_ids}` | `{:ok, group}` |
-| `Property` | `:validate_value` | Validate property value | 🔐 | `{property_type_id, value}` | `{:ok, valid}` or `{:error, reason}` |
+| `CustomField` | `:set_default_visibility` | Show/hide by default | 🛡️ | `{id, visible}` | `{:ok, custom_field}` |
+| `CustomField` | `:set_required` | Mark as required | 🛡️ | `{id, required}` | `{:ok, custom_field}` |
+| `CustomField` | `:add_validation` | Add validation rule | 🛡️ | `{id, rule_type, params}` | `{:ok, custom_field}` |
+| `CustomField` | `:create_group` | Create field group | 🛡️ | `{name, custom_field_ids}` | `{:ok, group}` |
+| `CustomFieldValue` | `:validate_value` | Validate custom field value | 🔐 | `{custom_field_id, value}` | `{:ok, valid}` or `{:error, reason}` |
---
diff --git a/lib/membership/custom_field.ex b/lib/membership/custom_field.ex
new file mode 100644
index 0000000..90bbcaa
--- /dev/null
+++ b/lib/membership/custom_field.ex
@@ -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
diff --git a/lib/membership/custom_field_value.ex b/lib/membership/custom_field_value.ex
new file mode 100644
index 0000000..2d6c025
--- /dev/null
+++ b/lib/membership/custom_field_value.ex
@@ -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
diff --git a/lib/membership/email.ex b/lib/membership/email.ex
index dccec21..730ccd7 100644
--- a/lib/membership/email.ex
+++ b/lib/membership/email.ex
@@ -4,22 +4,23 @@ defmodule Mv.Membership.Email do
## Overview
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.
## 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)
- Pattern: Standard email format (username@domain.tld)
- - Automatic trimming of leading/trailing whitespace
+ - Automatic trimming of leading/trailing whitespace (empty strings become `nil`)
## Usage
- This type is used in the Property union type for properties with
- `value_type: :email` in PropertyType definitions.
+ This type is used in the CustomFieldValue union type for custom fields with
+ `value_type: :email` in CustomField definitions.
## Example
- # In a property type definition
- PropertyType.create!(%{
+ # In a custom field definition
+ CustomField.create!(%{
name: "work_email",
value_type: :email
})
@@ -46,11 +47,18 @@ defmodule Mv.Membership.Email do
max_length: @max_length
]
+ @impl true
+ def cast_input(nil, _), do: {:ok, nil}
+
@impl true
def cast_input(value, _) when is_binary(value) do
value = String.trim(value)
cond do
+ # Empty string after trim becomes nil (optional field)
+ value == "" ->
+ {:ok, nil}
+
String.length(value) < @min_length ->
:error
diff --git a/lib/membership/member.ex b/lib/membership/member.ex
index 26c876f..eeb12c9 100644
--- a/lib/membership/member.ex
+++ b/lib/membership/member.ex
@@ -7,7 +7,7 @@ defmodule Mv.Membership.Member do
can have:
- Personal information (name, email, phone, address)
- 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
## Email Synchronization
@@ -16,7 +16,7 @@ defmodule Mv.Membership.Member do
See `Mv.EmailSync` for details.
## Relationships
- - `has_many :properties` - Dynamic custom fields
+ - `has_many :custom_field_values` - Dynamic custom fields
- `has_one :user` - Optional authentication account link
## Validations
@@ -48,8 +48,8 @@ defmodule Mv.Membership.Member do
create :create_member do
primary? true
- # Properties can be created along with member
- argument :properties, {:array, :map}
+ # Custom field values can be created along with member
+ argument :custom_field_values, {:array, :map}
# Allow user to be passed as argument for relationship management
# user_id is NOT in accept list to prevent direct foreign key manipulation
argument :user, :map, allow_nil?: true
@@ -70,7 +70,7 @@ defmodule Mv.Membership.Member do
:postal_code
]
- change manage_relationship(:properties, type: :create)
+ change manage_relationship(:custom_field_values, type: :create)
# Manage the user relationship during member creation
change manage_relationship(:user, :user,
@@ -95,8 +95,8 @@ defmodule Mv.Membership.Member do
primary? true
# Required because custom validation function cannot be done atomically
require_atomic? false
- # Properties can be updated or created along with member
- argument :properties, {:array, :map}
+ # Custom field values can be updated or created along with member
+ argument :custom_field_values, {:array, :map}
# Allow user to be passed as argument for relationship management
# user_id is NOT in accept list to prevent direct foreign key manipulation
argument :user, :map, allow_nil?: true
@@ -117,7 +117,7 @@ defmodule Mv.Membership.Member do
: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
change manage_relationship(:user, :user,
@@ -349,7 +349,7 @@ defmodule Mv.Membership.Member do
end
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
# This references the User's member_id attribute
# The relationship is optional (allow_nil? true by default)
diff --git a/lib/membership/membership.ex b/lib/membership/membership.ex
index 01de11b..f51c2b9 100644
--- a/lib/membership/membership.ex
+++ b/lib/membership/membership.ex
@@ -3,15 +3,15 @@ defmodule Mv.Membership do
Ash Domain for membership management.
## Resources
- - `Member` - Club members with personal information and custom properties
- - `Property` - Dynamic custom field values attached to members
- - `PropertyType` - Schema definitions for custom properties
+ - `Member` - Club members with personal information and custom field values
+ - `CustomFieldValue` - Dynamic custom field values attached to members
+ - `CustomField` - Schema definitions for custom fields
## Public API
The domain exposes these main actions:
- Member CRUD: `create_member/1`, `list_members/0`, `update_member/2`, `destroy_member/1`
- - Property management: `create_property/1`, `list_property/0`, etc.
- - PropertyType management: `create_property_type/1`, `list_property_types/0`, etc.
+ - Custom field value management: `create_custom_field_value/1`, `list_custom_field_values/0`, etc.
+ - Custom field management: `create_custom_field/1`, `list_custom_fields/0`, etc.
## Admin Interface
The domain is configured with AshAdmin for management UI.
@@ -31,18 +31,18 @@ defmodule Mv.Membership do
define :destroy_member, action: :destroy
end
- resource Mv.Membership.Property do
- define :create_property, action: :create
- define :list_property, action: :read
- define :update_property, action: :update
- define :destroy_property, action: :destroy
+ resource Mv.Membership.CustomFieldValue do
+ define :create_custom_field_value, action: :create
+ define :list_custom_field_values, action: :read
+ define :update_custom_field_value, action: :update
+ define :destroy_custom_field_value, action: :destroy
end
- resource Mv.Membership.PropertyType do
- define :create_property_type, action: :create
- define :list_property_types, action: :read
- define :update_property_type, action: :update
- define :destroy_property_type, action: :destroy
+ resource Mv.Membership.CustomField do
+ define :create_custom_field, action: :create
+ define :list_custom_fields, action: :read
+ define :update_custom_field, action: :update
+ define :destroy_custom_field, action: :destroy
end
end
end
diff --git a/lib/membership/property.ex b/lib/membership/property.ex
deleted file mode 100644
index 231b264..0000000
--- a/lib/membership/property.ex
+++ /dev/null
@@ -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
diff --git a/lib/membership/property_type.ex b/lib/membership/property_type.ex
deleted file mode 100644
index 6569d1b..0000000
--- a/lib/membership/property_type.ex
+++ /dev/null
@@ -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
diff --git a/lib/mv_web/components/layouts/navbar.ex b/lib/mv_web/components/layouts/navbar.ex
index 9fec3f4..1de4c7f 100644
--- a/lib/mv_web/components/layouts/navbar.ex
+++ b/lib/mv_web/components/layouts/navbar.ex
@@ -17,6 +17,7 @@ defmodule MvWeb.Layouts.Navbar do
Mitgliederverwaltung
diff --git a/lib/mv_web/live/property_type_live/form.ex b/lib/mv_web/live/custom_field_live/form.ex
similarity index 57%
rename from lib/mv_web/live/property_type_live/form.ex
rename to lib/mv_web/live/custom_field_live/form.ex
index 292de2b..b1d3f86 100644
--- a/lib/mv_web/live/property_type_live/form.ex
+++ b/lib/mv_web/live/custom_field_live/form.ex
@@ -1,10 +1,10 @@
-defmodule MvWeb.PropertyTypeLive.Form do
+defmodule MvWeb.CustomFieldLive.Form do
@moduledoc """
- LiveView form for creating and editing property types (admin).
+ LiveView form for creating and editing custom fields (admin).
## Features
- - Create new property type definitions
- - Edit existing property types
+ - Create new custom field definitions
+ - Edit existing custom fields
- Select value type from supported types
- Set immutable and required flags
- Real-time validation
@@ -17,7 +17,7 @@ defmodule MvWeb.PropertyTypeLive.Form do
**Optional:**
- description - Human-readable explanation
- 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
- `:string` - Text data (unlimited length)
@@ -28,10 +28,10 @@ defmodule MvWeb.PropertyTypeLive.Form do
## Events
- `validate` - Real-time form validation
- - `save` - Submit form (create or update property type)
+ - `save` - Submit form (create or update custom field)
## Security
- Property type management is restricted to admin users.
+ Custom field management is restricted to admin users.
"""
use MvWeb, :live_view
@@ -42,18 +42,18 @@ defmodule MvWeb.PropertyTypeLive.Form do
<.header>
{@page_title}
<: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.")}
- <.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[:value_type]}
type="select"
label={gettext("Value type")}
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")} />
@@ -61,9 +61,9 @@ defmodule MvWeb.PropertyTypeLive.Form do
<.input field={@form[:required]} type="checkbox" label={gettext("Required")} />
<.button phx-disable-with={gettext("Saving...")} variant="primary">
- {gettext("Save Property type")}
+ {gettext("Save Custom field")}
- <.button navigate={return_path(@return_to, @property_type)}>{gettext("Cancel")}
+ <.button navigate={return_path(@return_to, @custom_field)}>{gettext("Cancel")}
"""
@@ -71,19 +71,19 @@ defmodule MvWeb.PropertyTypeLive.Form do
@impl true
def mount(params, _session, socket) do
- property_type =
+ custom_field =
case params["id"] do
nil -> nil
- id -> Ash.get!(Mv.Membership.PropertyType, id)
+ id -> Ash.get!(Mv.Membership.CustomField, id)
end
- action = if is_nil(property_type), do: "New", else: "Edit"
- page_title = action <> " " <> "Property type"
+ action = if is_nil(custom_field), do: "New", else: "Edit"
+ page_title = action <> " " <> "Custom field"
{:ok,
socket
|> assign(:return_to, return_to(params["return_to"]))
- |> assign(property_type: property_type)
+ |> assign(custom_field: custom_field)
|> assign(:page_title, page_title)
|> assign_form()}
end
@@ -92,15 +92,15 @@ defmodule MvWeb.PropertyTypeLive.Form do
defp return_to(_), do: "index"
@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,
- 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
- def handle_event("save", %{"property_type" => property_type_params}, socket) do
- case AshPhoenix.Form.submit(socket.assigns.form, params: property_type_params) do
- {:ok, property_type} ->
- notify_parent({:saved, property_type})
+ def handle_event("save", %{"custom_field" => custom_field_params}, socket) do
+ case AshPhoenix.Form.submit(socket.assigns.form, params: custom_field_params) do
+ {:ok, custom_field} ->
+ notify_parent({:saved, custom_field})
action =
case socket.assigns.form.source.type do
@@ -111,8 +111,8 @@ defmodule MvWeb.PropertyTypeLive.Form do
socket =
socket
- |> put_flash(:info, gettext("Property type %{action} successfully", action: action))
- |> push_navigate(to: return_path(socket.assigns.return_to, property_type))
+ |> put_flash(:info, gettext("Custom field %{action} successfully", action: action))
+ |> push_navigate(to: return_path(socket.assigns.return_to, custom_field))
{:noreply, socket}
@@ -123,17 +123,17 @@ defmodule MvWeb.PropertyTypeLive.Form do
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 =
- if property_type do
- AshPhoenix.Form.for_update(property_type, :update, as: "property_type")
+ if custom_field do
+ AshPhoenix.Form.for_update(custom_field, :update, as: "custom_field")
else
- AshPhoenix.Form.for_create(Mv.Membership.PropertyType, :create, as: "property_type")
+ AshPhoenix.Form.for_create(Mv.Membership.CustomField, :create, as: "custom_field")
end
assign(socket, form: to_form(form))
end
- defp return_path("index", _property_type), do: ~p"/property_types"
- defp return_path("show", property_type), do: ~p"/property_types/#{property_type.id}"
+ defp return_path("index", _custom_field), do: ~p"/custom_fields"
+ defp return_path("show", custom_field), do: ~p"/custom_fields/#{custom_field.id}"
end
diff --git a/lib/mv_web/live/custom_field_live/index.ex b/lib/mv_web/live/custom_field_live/index.ex
new file mode 100644
index 0000000..2870611
--- /dev/null
+++ b/lib/mv_web/live/custom_field_live/index.ex
@@ -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"""
+
+ <.header>
+ Listing Custom fields
+ <:actions>
+ <.button variant="primary" navigate={~p"/custom_fields/new"}>
+ <.icon name="hero-plus" /> New Custom field
+
+
+
+
+ <.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 :let={{_id, custom_field}} label="Name">{custom_field.name}
+
+ <:col :let={{_id, custom_field}} label="Description">{custom_field.description}
+
+ <:action :let={{_id, custom_field}}>
+
+ <.link navigate={~p"/custom_fields/#{custom_field}"}>Show
+
+
+ <.link navigate={~p"/custom_fields/#{custom_field}/edit"}>Edit
+
+
+ <:action :let={{id, custom_field}}>
+ <.link
+ phx-click={JS.push("delete", value: %{id: custom_field.id}) |> hide("##{id}")}
+ data-confirm="Are you sure?"
+ >
+ Delete
+
+
+
+
+ """
+ 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
diff --git a/lib/mv_web/live/custom_field_live/show.ex b/lib/mv_web/live/custom_field_live/show.ex
new file mode 100644
index 0000000..783cb4e
--- /dev/null
+++ b/lib/mv_web/live/custom_field_live/show.ex
@@ -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"""
+
+ <.header>
+ Custom field {@custom_field.id}
+ <:subtitle>This is a custom_field record from your database.
+
+ <:actions>
+ <.button navigate={~p"/custom_fields"}>
+ <.icon name="hero-arrow-left" />
+
+ <.button
+ variant="primary"
+ navigate={~p"/custom_fields/#{@custom_field}/edit?return_to=show"}
+ >
+ <.icon name="hero-pencil-square" /> Edit Custom field
+
+
+
+
+ <.list>
+ <:item title="Id">{@custom_field.id}
+
+ <:item title="Name">{@custom_field.name}
+
+ <:item title="Description">{@custom_field.description}
+
+
+ """
+ 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
diff --git a/lib/mv_web/live/property_live/form.ex b/lib/mv_web/live/custom_field_value_live/form.ex
similarity index 54%
rename from lib/mv_web/live/property_live/form.ex
rename to lib/mv_web/live/custom_field_value_live/form.ex
index b85597d..7df4c69 100644
--- a/lib/mv_web/live/property_live/form.ex
+++ b/lib/mv_web/live/custom_field_value_live/form.ex
@@ -1,21 +1,21 @@
-defmodule MvWeb.PropertyLive.Form do
+defmodule MvWeb.CustomFieldValueLive.Form do
@moduledoc """
- LiveView form for creating and editing properties.
+ LiveView form for creating and editing custom field values.
## Features
- - Create new properties with member and type selection
- - Edit existing property values
- - Value input adapts to property type (string, integer, boolean, date, email)
+ - Create new custom field values with member and type selection
+ - Edit existing custom field values
+ - Value input adapts to custom field type (string, integer, boolean, date, email)
- Real-time validation
## Form Fields
**Required:**
- - member - Select which member owns this property
- - property_type - Select the type (defines value type)
- - value - The actual value (input type depends on property type)
+ - member - Select which member owns this custom field value
+ - custom_field - Select the type (defines value type)
+ - value - The actual value (input type depends on custom field type)
## 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
- Integer: number input
- Boolean: checkbox
@@ -24,10 +24,10 @@ defmodule MvWeb.PropertyLive.Form do
## Events
- `validate` - Real-time form validation
- - `save` - Submit form (create or update property)
+ - `save` - Submit form (create or update custom field value)
## 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.
"""
use MvWeb, :live_view
@@ -38,17 +38,19 @@ defmodule MvWeb.PropertyLive.Form do
<.header>
{@page_title}
- <:subtitle>{gettext("Use this form to manage property records in your database.")}
+ <:subtitle>
+ {gettext("Use this form to manage custom_field_value records in your database.")}
+
- <.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">
+
<.input
- field={@form[:property_type_id]}
+ field={@form[:custom_field_id]}
type="select"
- label={gettext("Property type")}
- options={property_type_options(@property_types)}
- prompt={gettext("Choose a property type")}
+ label={gettext("Custom field")}
+ options={custom_field_options(@custom_fields)}
+ prompt={gettext("Choose a custom field")}
/>
@@ -61,18 +63,18 @@ defmodule MvWeb.PropertyLive.Form do
/>
- <%= if @selected_property_type do %>
- <.union_value_input form={@form} property_type={@selected_property_type} />
+ <%= if @selected_custom_field do %>
+ <.union_value_input form={@form} custom_field={@selected_custom_field} />
<% else %>
- {gettext("Please select a property type first")}
+ {gettext("Please select a custom field first")}
<% end %>
<.button phx-disable-with={gettext("Saving...")} variant="primary">
- {gettext("Save Property")}
+ {gettext("Save Custom field value")}
- <.button navigate={return_path(@return_to, @property)}>{gettext("Cancel")}
+ <.button navigate={return_path(@return_to, @custom_field_value)}>{gettext("Cancel")}
"""
@@ -80,8 +82,8 @@ defmodule MvWeb.PropertyLive.Form do
# Helper function for Union-Value Input
defp union_value_input(assigns) do
- # Extract the current value from the Property
- current_value = extract_current_value(assigns.form.data, assigns.property_type.value_type)
+ # Extract the current value from the CustomFieldValue
+ current_value = extract_current_value(assigns.form.data, assigns.custom_field.value_type)
assigns = assign(assigns, :current_value, current_value)
~H"""
@@ -90,7 +92,7 @@ defmodule MvWeb.PropertyLive.Form do
{gettext("Value")}
- <%= case @property_type.value_type do %>
+ <%= case @custom_field.value_type do %>
<% :string -> %>
<.inputs_for :let={value_form} field={@form[:value]}>
<.input field={value_form[:value]} type="text" label="" value={@current_value} />
@@ -123,16 +125,16 @@ defmodule MvWeb.PropertyLive.Form do
<% _ -> %>
- {gettext("Unsupported value type: %{type}", type: @property_type.value_type)}
+ {gettext("Unsupported value type: %{type}", type: @custom_field.value_type)}
<% 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(
- %Mv.Membership.Property{value: %Ash.Union{value: value}},
+ %Mv.Membership.CustomFieldValue{value: %Ash.Union{value: value}},
_value_type
) do
value
@@ -160,27 +162,27 @@ defmodule MvWeb.PropertyLive.Form do
@impl true
def mount(params, _session, socket) do
- property =
+ custom_field_value =
case params["id"] do
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
- action = if is_nil(property), do: "New", else: "Edit"
- page_title = action <> " " <> "Property"
+ action = if is_nil(custom_field_value), do: "New", else: "Edit"
+ page_title = action <> " " <> "Custom field value"
- # Load all PropertyTypes and Members for the selection fields
- property_types = Ash.read!(Mv.Membership.PropertyType)
+ # Load all CustomFields and Members for the selection fields
+ custom_fields = Ash.read!(Mv.Membership.CustomField)
members = Ash.read!(Mv.Membership.Member)
{:ok,
socket
|> assign(:return_to, return_to(params["return_to"]))
- |> assign(property: property)
+ |> assign(custom_field_value: custom_field_value)
|> assign(:page_title, page_title)
- |> assign(:property_types, property_types)
+ |> assign(:custom_fields, custom_fields)
|> 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()}
end
@@ -188,43 +190,43 @@ defmodule MvWeb.PropertyLive.Form do
defp return_to(_), do: "index"
@impl true
- def handle_event("validate", %{"property" => property_params}, socket) do
- # Find the selected PropertyType
- selected_property_type =
- case property_params["property_type_id"] do
+ def handle_event("validate", %{"custom_field_value" => custom_field_value_params}, socket) do
+ # Find the selected CustomField
+ selected_custom_field =
+ case custom_field_value_params["custom_field_id"] do
"" -> nil
nil -> nil
- id -> Enum.find(socket.assigns.property_types, &(&1.id == id))
+ id -> Enum.find(socket.assigns.custom_fields, &(&1.id == id))
end
- # Set the Union type based on the selected PropertyType
+ # Set the Union type based on the selected CustomField
updated_params =
- if selected_property_type do
- union_type = to_string(selected_property_type.value_type)
- put_in(property_params, ["value", "_union_type"], union_type)
+ if selected_custom_field do
+ union_type = to_string(selected_custom_field.value_type)
+ put_in(custom_field_value_params, ["value", "_union_type"], union_type)
else
- property_params
+ custom_field_value_params
end
{:noreply,
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))}
end
- def handle_event("save", %{"property" => property_params}, socket) do
- # Set the Union type based on the selected PropertyType
+ def handle_event("save", %{"custom_field_value" => custom_field_value_params}, socket) do
+ # Set the Union type based on the selected CustomField
updated_params =
- if socket.assigns.selected_property_type do
- union_type = to_string(socket.assigns.selected_property_type.value_type)
- put_in(property_params, ["value", "_union_type"], union_type)
+ if socket.assigns.selected_custom_field do
+ union_type = to_string(socket.assigns.selected_custom_field.value_type)
+ put_in(custom_field_value_params, ["value", "_union_type"], union_type)
else
- property_params
+ custom_field_value_params
end
case AshPhoenix.Form.submit(socket.assigns.form, params: updated_params) do
- {:ok, property} ->
- notify_parent({:saved, property})
+ {:ok, custom_field_value} ->
+ notify_parent({:saved, custom_field_value})
action =
case socket.assigns.form.source.type do
@@ -235,8 +237,11 @@ defmodule MvWeb.PropertyLive.Form do
socket =
socket
- |> put_flash(:info, gettext("Property %{action} successfully", action: action))
- |> push_navigate(to: return_path(socket.assigns.return_to, property))
+ |> put_flash(
+ :info,
+ gettext("Custom field value %{action} successfully", action: action)
+ )
+ |> push_navigate(to: return_path(socket.assigns.return_to, custom_field_value))
{:noreply, socket}
@@ -247,11 +252,11 @@ defmodule MvWeb.PropertyLive.Form do
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 =
- if property do
- # Determine the Union type based on the property_type
- union_type = property.property_type && property.property_type.value_type
+ if custom_field_value do
+ # Determine the Union type based on the custom_field
+ union_type = custom_field_value.custom_field && custom_field_value.custom_field.value_type
params =
if union_type do
@@ -260,20 +265,27 @@ defmodule MvWeb.PropertyLive.Form do
%{}
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
- AshPhoenix.Form.for_create(Mv.Membership.Property, :create, as: "property")
+ AshPhoenix.Form.for_create(Mv.Membership.CustomFieldValue, :create,
+ as: "custom_field_value"
+ )
end
assign(socket, form: to_form(form))
end
- defp return_path("index", _property), do: ~p"/properties"
- defp return_path("show", property), do: ~p"/properties/#{property.id}"
+ defp return_path("index", _custom_field_value), do: ~p"/custom_field_values"
+
+ defp return_path("show", custom_field_value),
+ do: ~p"/custom_field_values/#{custom_field_value.id}"
# Helper functions for selection options
- defp property_type_options(property_types) do
- Enum.map(property_types, &{&1.name, &1.id})
+ defp custom_field_options(custom_fields) do
+ Enum.map(custom_fields, &{&1.name, &1.id})
end
defp member_options(members) do
diff --git a/lib/mv_web/live/custom_field_value_live/index.ex b/lib/mv_web/live/custom_field_value_live/index.ex
new file mode 100644
index 0000000..b52fd96
--- /dev/null
+++ b/lib/mv_web/live/custom_field_value_live/index.ex
@@ -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"""
+
+ <.header>
+ Listing Custom field values
+ <:actions>
+ <.button variant="primary" navigate={~p"/custom_field_values/new"}>
+ <.icon name="hero-plus" /> New Custom field value
+
+
+
+
+ <.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}
+
+ <:action :let={{_id, custom_field_value}}>
+
+ <.link navigate={~p"/custom_field_values/#{custom_field_value}"}>Show
+
+
+ <.link navigate={~p"/custom_field_values/#{custom_field_value}/edit"}>Edit
+
+
+ <: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
+
+
+
+
+ """
+ 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
diff --git a/lib/mv_web/live/custom_field_value_live/show.ex b/lib/mv_web/live/custom_field_value_live/show.ex
new file mode 100644
index 0000000..42e9f43
--- /dev/null
+++ b/lib/mv_web/live/custom_field_value_live/show.ex
@@ -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"""
+
+ <.header>
+ Custom field value {@custom_field_value.id}
+ <:subtitle>This is a custom_field_value record from your database.
+
+ <:actions>
+ <.button navigate={~p"/custom_field_values"}>
+ <.icon name="hero-arrow-left" />
+
+ <.button
+ variant="primary"
+ navigate={~p"/custom_field_values/#{@custom_field_value}/edit?return_to=show"}
+ >
+ <.icon name="hero-pencil-square" /> Edit Custom field value
+
+
+
+
+ <.list>
+ <:item title="Id">{@custom_field_value.id}
+
+
+ """
+ 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
diff --git a/lib/mv_web/live/member_live/form.ex b/lib/mv_web/live/member_live/form.ex
index ba7ba36..e4c2e7e 100644
--- a/lib/mv_web/live/member_live/form.ex
+++ b/lib/mv_web/live/member_live/form.ex
@@ -19,14 +19,14 @@ defmodule MvWeb.MemberLive.Form do
- paid status
- notes
- ## Custom Properties
- Members can have dynamic custom properties defined by PropertyTypes.
- The form dynamically renders inputs based on available PropertyTypes.
+ ## Custom Field Values
+ Members can have dynamic custom field values defined by CustomFields.
+ The form dynamically renders inputs based on available CustomFields.
## Events
- `validate` - Real-time form validation
- `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
@@ -56,10 +56,11 @@ defmodule MvWeb.MemberLive.Form do
<.input field={@form[:house_number]} label={gettext("House Number")} />
<.input field={@form[:postal_code]} label={gettext("Postal Code")} />
- {gettext("Custom Properties")}
- <.inputs_for :let={f_property} field={@form[:properties]}>
- <% type = Enum.find(@property_types, &(&1.id == f_property[:property_type_id].value)) %>
- <.inputs_for :let={value_form} field={f_property[:value]}>
+ {gettext("Custom Field Values")}
+ <.inputs_for :let={f_custom_field_value} field={@form[:custom_field_values]}>
+ <% type =
+ 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 =
cond do
type && type.value_type == :boolean -> "checkbox"
@@ -70,8 +71,8 @@ defmodule MvWeb.MemberLive.Form do
@@ -86,16 +87,16 @@ defmodule MvWeb.MemberLive.Form do
@impl true
def mount(params, _session, socket) do
- {:ok, property_types} = Mv.Membership.list_property_types()
+ {:ok, custom_fields} = Mv.Membership.list_custom_fields()
- initial_properties =
- Enum.map(property_types, fn pt ->
+ initial_custom_field_values =
+ Enum.map(custom_fields, fn cf ->
%{
- "property_type_id" => pt.id,
+ "custom_field_id" => cf.id,
"value" => %{
- "type" => pt.value_type,
+ "type" => cf.value_type,
"value" => nil,
- "_union_type" => Atom.to_string(pt.value_type)
+ "_union_type" => Atom.to_string(cf.value_type)
}
}
end)
@@ -112,8 +113,8 @@ defmodule MvWeb.MemberLive.Form do
{:ok,
socket
|> assign(:return_to, return_to(params["return_to"]))
- |> assign(:property_types, property_types)
- |> assign(:initial_properties, initial_properties)
+ |> assign(:custom_fields, custom_fields)
+ |> assign(:initial_custom_field_values, initial_custom_field_values)
|> assign(member: member)
|> assign(:page_title, page_title)
|> assign_form()}
@@ -156,25 +157,25 @@ defmodule MvWeb.MemberLive.Form do
defp assign_form(%{assigns: %{member: member}} = socket) do
form =
if member do
- {:ok, member} = Ash.load(member, properties: [:property_type])
+ {:ok, member} = Ash.load(member, custom_field_values: [:custom_field])
- existing_properties =
- member.properties
- |> Enum.map(& &1.property_type_id)
+ existing_custom_field_values =
+ member.custom_field_values
+ |> Enum.map(& &1.custom_field_id)
- is_missing_property = fn i ->
- not Enum.member?(existing_properties, Map.get(i, "property_type_id"))
+ is_missing_custom_field_value = fn i ->
+ not Enum.member?(existing_custom_field_values, Map.get(i, "custom_field_id"))
end
params = %{
- "properties" =>
- Enum.map(member.properties, fn prop ->
+ "custom_field_values" =>
+ Enum.map(member.custom_field_values, fn cfv ->
%{
- "property_type_id" => prop.property_type_id,
+ "custom_field_id" => cfv.custom_field_id,
"value" => %{
- "_union_type" => Atom.to_string(prop.value.type),
- "type" => prop.value.type,
- "value" => prop.value.value
+ "_union_type" => Atom.to_string(cfv.value.type),
+ "type" => cfv.value.type,
+ "value" => cfv.value.value
}
}
end)
@@ -190,12 +191,13 @@ defmodule MvWeb.MemberLive.Form do
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(
- missing_properties,
+ missing_custom_field_values,
form,
- &AshPhoenix.Form.add_form(&2, [:properties], params: &1)
+ &AshPhoenix.Form.add_form(&2, [:custom_field_values], params: &1)
)
else
AshPhoenix.Form.for_create(
@@ -203,7 +205,7 @@ defmodule MvWeb.MemberLive.Form do
:create_member,
api: Mv.Membership,
as: "member",
- params: %{"properties" => socket.assigns[:initial_properties]},
+ params: %{"custom_field_values" => socket.assigns[:initial_custom_field_values]},
forms: [auto?: true]
)
end
diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex
index 043915e..7ec24fa 100644
--- a/lib/mv_web/live/member_live/show.ex
+++ b/lib/mv_web/live/member_live/show.ex
@@ -5,7 +5,7 @@ defmodule MvWeb.MemberLive.Show do
## Features
- Display all member information (personal, contact, address)
- Show linked user account (if exists)
- - Display custom properties
+ - Display custom field values
- Navigate to edit form
- Return to member list
@@ -15,7 +15,7 @@ defmodule MvWeb.MemberLive.Show do
- Address: street, house number, postal code, city
- Status: paid flag
- Relationships: linked user account
- - Custom: dynamic properties from PropertyTypes
+ - Custom: dynamic custom field values from CustomFields
## Navigation
- Back to member list
@@ -75,14 +75,14 @@ defmodule MvWeb.MemberLive.Show do
- {gettext("Custom Properties")}
+ {gettext("Custom Field Values")}
<.generic_list items={
- Enum.map(@member.properties, fn p ->
+ Enum.map(@member.custom_field_values, fn cfv ->
{
# name
- p.property_type && p.property_type.name,
+ cfv.custom_field && cfv.custom_field.name,
# value
- case p.value do
+ case cfv.value do
%{value: v} -> v
v -> v
end
@@ -103,7 +103,7 @@ defmodule MvWeb.MemberLive.Show do
query =
Mv.Membership.Member
|> filter(id == ^id)
- |> load([:user, properties: [:property_type]])
+ |> load([:user, custom_field_values: [:custom_field]])
member = Ash.read_one!(query)
diff --git a/lib/mv_web/live/property_live/index.ex b/lib/mv_web/live/property_live/index.ex
deleted file mode 100644
index bc96bc0..0000000
--- a/lib/mv_web/live/property_live/index.ex
+++ /dev/null
@@ -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"""
-
- <.header>
- Listing Properties
- <:actions>
- <.button variant="primary" navigate={~p"/properties/new"}>
- <.icon name="hero-plus" /> New Property
-
-
-
-
- <.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}
-
- <:action :let={{_id, property}}>
-
- <.link navigate={~p"/properties/#{property}"}>Show
-
-
- <.link navigate={~p"/properties/#{property}/edit"}>Edit
-
-
- <:action :let={{id, property}}>
- <.link
- phx-click={JS.push("delete", value: %{id: property.id}) |> hide("##{id}")}
- data-confirm="Are you sure?"
- >
- Delete
-
-
-
-
- """
- 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
diff --git a/lib/mv_web/live/property_live/show.ex b/lib/mv_web/live/property_live/show.ex
deleted file mode 100644
index 41e20c4..0000000
--- a/lib/mv_web/live/property_live/show.ex
+++ /dev/null
@@ -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"""
-
- <.header>
- Property {@property.id}
- <:subtitle>This is a property record from your database.
-
- <:actions>
- <.button navigate={~p"/properties"}>
- <.icon name="hero-arrow-left" />
-
- <.button variant="primary" navigate={~p"/properties/#{@property}/edit?return_to=show"}>
- <.icon name="hero-pencil-square" /> Edit Property
-
-
-
-
- <.list>
- <:item title="Id">{@property.id}
-
-
- """
- 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
diff --git a/lib/mv_web/live/property_type_live/index.ex b/lib/mv_web/live/property_type_live/index.ex
deleted file mode 100644
index 2731414..0000000
--- a/lib/mv_web/live/property_type_live/index.ex
+++ /dev/null
@@ -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"""
-
- <.header>
- Listing Property types
- <:actions>
- <.button variant="primary" navigate={~p"/property_types/new"}>
- <.icon name="hero-plus" /> New Property type
-
-
-
-
- <.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 :let={{_id, property_type}} label="Name">{property_type.name}
-
- <:col :let={{_id, property_type}} label="Description">{property_type.description}
-
- <:action :let={{_id, property_type}}>
-
- <.link navigate={~p"/property_types/#{property_type}"}>Show
-
-
- <.link navigate={~p"/property_types/#{property_type}/edit"}>Edit
-
-
- <:action :let={{id, property_type}}>
- <.link
- phx-click={JS.push("delete", value: %{id: property_type.id}) |> hide("##{id}")}
- data-confirm="Are you sure?"
- >
- Delete
-
-
-
-
- """
- 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
diff --git a/lib/mv_web/live/property_type_live/show.ex b/lib/mv_web/live/property_type_live/show.ex
deleted file mode 100644
index b5c441c..0000000
--- a/lib/mv_web/live/property_type_live/show.ex
+++ /dev/null
@@ -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"""
-
- <.header>
- Property type {@property_type.id}
- <:subtitle>This is a property_type record from your database.
-
- <:actions>
- <.button navigate={~p"/property_types"}>
- <.icon name="hero-arrow-left" />
-
- <.button
- variant="primary"
- navigate={~p"/property_types/#{@property_type}/edit?return_to=show"}
- >
- <.icon name="hero-pencil-square" /> Edit Property type
-
-
-
-
- <.list>
- <:item title="Id">{@property_type.id}
-
- <:item title="Name">{@property_type.name}
-
- <:item title="Description">{@property_type.description}
-
-
- """
- 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
diff --git a/lib/mv_web/router.ex b/lib/mv_web/router.ex
index a08f1be..d2a63bc 100644
--- a/lib/mv_web/router.ex
+++ b/lib/mv_web/router.ex
@@ -55,17 +55,17 @@ defmodule MvWeb.Router do
live "/members/:id", MemberLive.Show, :show
live "/members/:id/show/edit", MemberLive.Show, :edit
- live "/property_types", PropertyTypeLive.Index, :index
- live "/property_types/new", PropertyTypeLive.Form, :new
- live "/property_types/:id/edit", PropertyTypeLive.Form, :edit
- live "/property_types/:id", PropertyTypeLive.Show, :show
- live "/property_types/:id/show/edit", PropertyTypeLive.Show, :edit
+ live "/custom_fields", CustomFieldLive.Index, :index
+ live "/custom_fields/new", CustomFieldLive.Form, :new
+ live "/custom_fields/:id/edit", CustomFieldLive.Form, :edit
+ live "/custom_fields/:id", CustomFieldLive.Show, :show
+ live "/custom_fields/:id/show/edit", CustomFieldLive.Show, :edit
- live "/properties", PropertyLive.Index, :index
- live "/properties/new", PropertyLive.Form, :new
- live "/properties/:id/edit", PropertyLive.Form, :edit
- live "/properties/:id", PropertyLive.Show, :show
- live "/properties/:id/show/edit", PropertyLive.Show, :edit
+ live "/custom_field_values", CustomFieldValueLive.Index, :index
+ live "/custom_field_values/new", CustomFieldValueLive.Form, :new
+ live "/custom_field_values/:id/edit", CustomFieldValueLive.Form, :edit
+ live "/custom_field_values/:id", CustomFieldValueLive.Show, :show
+ live "/custom_field_values/:id/show/edit", CustomFieldValueLive.Show, :edit
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Form, :new
diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po
index 22ff795..f6acdca 100644
--- a/priv/gettext/de/LC_MESSAGES/default.po
+++ b/priv/gettext/de/LC_MESSAGES/default.po
@@ -27,9 +27,9 @@ msgstr "Bist du sicher?"
msgid "Attempting to reconnect"
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/show.ex:37
+#: lib/mv_web/live/member_live/show.ex:59
#, elixir-autogen, elixir-format
msgid "City"
msgstr "Stadt"
@@ -41,43 +41,43 @@ msgid "Delete"
msgstr "Löschen"
#: 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
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr "Bearbeite"
-#: lib/mv_web/live/member_live/show.ex:19
-#: lib/mv_web/live/member_live/show.ex:95
+#: lib/mv_web/live/member_live/show.ex:41
+#: lib/mv_web/live/member_live/show.ex:117
#, elixir-autogen, elixir-format
msgid "Edit Member"
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/show.ex:28
-#: lib/mv_web/live/user_live/form.ex:14
+#: lib/mv_web/live/member_live/show.ex:50
+#: lib/mv_web/live/user_live/form.ex:46
#: lib/mv_web/live/user_live/index.html.heex:44
-#: lib/mv_web/live/user_live/show.ex:25
+#: lib/mv_web/live/user_live/show.ex:50
#, elixir-autogen, elixir-format
msgid "Email"
msgstr "E-Mail"
-#: lib/mv_web/live/member_live/form.ex:16
-#: lib/mv_web/live/member_live/show.ex:26
+#: lib/mv_web/live/member_live/form.ex:45
+#: lib/mv_web/live/member_live/show.ex:48
#, elixir-autogen, elixir-format
msgid "First Name"
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/show.ex:34
+#: lib/mv_web/live/member_live/show.ex:56
#, elixir-autogen, elixir-format
msgid "Join Date"
msgstr "Beitrittsdatum"
-#: lib/mv_web/live/member_live/form.ex:17
-#: lib/mv_web/live/member_live/show.ex:27
+#: lib/mv_web/live/member_live/form.ex:46
+#: lib/mv_web/live/member_live/show.ex:49
#, elixir-autogen, elixir-format
msgid "Last Name"
msgstr "Nachname"
@@ -108,117 +108,111 @@ msgstr "Keine Internetverbindung gefunden"
msgid "close"
msgstr "schließen"
-#: lib/mv_web/live/member_live/form.ex:19
-#: lib/mv_web/live/member_live/show.ex:29
+#: lib/mv_web/live/member_live/form.ex:48
+#: lib/mv_web/live/member_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "Birth Date"
msgstr "Geburtsdatum"
-#: lib/mv_web/live/member_live/form.ex:30
-#: lib/mv_web/live/member_live/show.ex:56
-#, 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
+#: lib/mv_web/live/member_live/form.ex:52
+#: lib/mv_web/live/member_live/show.ex:57
#, elixir-autogen, elixir-format
msgid "Exit Date"
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/show.ex:39
+#: lib/mv_web/live/member_live/show.ex:61
#, elixir-autogen, elixir-format
msgid "House Number"
msgstr "Hausnummer"
-#: lib/mv_web/live/member_live/form.ex:24
-#: lib/mv_web/live/member_live/show.ex:36
+#: lib/mv_web/live/member_live/form.ex:53
+#: lib/mv_web/live/member_live/show.ex:58
#, elixir-autogen, elixir-format
msgid "Notes"
msgstr "Notizen"
-#: lib/mv_web/live/member_live/form.ex:20
-#: lib/mv_web/live/member_live/show.ex:30
+#: lib/mv_web/live/member_live/form.ex:49
+#: lib/mv_web/live/member_live/show.ex:52
#, elixir-autogen, elixir-format
msgid "Paid"
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/show.ex:33
+#: lib/mv_web/live/member_live/show.ex:55
#, elixir-autogen, elixir-format
msgid "Phone Number"
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/show.ex:40
+#: lib/mv_web/live/member_live/show.ex:62
#, elixir-autogen, elixir-format
msgid "Postal Code"
msgstr "Postleitzahl"
-#: lib/mv_web/live/member_live/form.ex:50
+#: lib/mv_web/live/member_live/form.ex:80
#, elixir-autogen, elixir-format
msgid "Save Member"
msgstr "Mitglied speichern"
-#: lib/mv_web/live/member_live/form.ex:49
-#: lib/mv_web/live/property_live/form.ex:41
-#: lib/mv_web/live/property_type_live/form.ex:29
-#: lib/mv_web/live/user_live/form.ex:92
+#: lib/mv_web/live/custom_field_live/form.ex:63
+#: lib/mv_web/live/custom_field_value_live/form.ex:74
+#: lib/mv_web/live/member_live/form.ex:79
+#: lib/mv_web/live/user_live/form.ex:124
#, elixir-autogen, elixir-format
msgid "Saving..."
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/show.ex:38
+#: lib/mv_web/live/member_live/show.ex:60
#, elixir-autogen, elixir-format
msgid "Street"
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
msgid "Use this form to manage member records and their properties."
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
msgid "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
msgid "No"
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
msgid "Show Member"
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
msgid "This is a member record from your database."
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
msgid "Yes"
msgstr "Ja"
-#: lib/mv_web/live/member_live/form.ex:108
-#: lib/mv_web/live/property_live/form.ex:200
-#: lib/mv_web/live/property_type_live/form.ex:73
+#: lib/mv_web/live/custom_field_live/form.ex:107
+#: lib/mv_web/live/custom_field_value_live/form.ex:233
+#: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format
msgid "create"
msgstr "erstellt"
-#: lib/mv_web/live/member_live/form.ex:109
-#: lib/mv_web/live/property_live/form.ex:201
-#: lib/mv_web/live/property_type_live/form.ex:74
+#: lib/mv_web/live/custom_field_live/form.ex:108
+#: lib/mv_web/live/custom_field_value_live/form.ex:234
+#: lib/mv_web/live/member_live/form.ex:139
#, elixir-autogen, elixir-format
msgid "update"
msgstr "aktualisiert"
@@ -228,7 +222,7 @@ msgstr "aktualisiert"
msgid "Incorrect email or password"
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
msgid "Member %{action} successfully"
msgstr "Mitglied %{action} erfolgreich"
@@ -258,73 +252,68 @@ msgstr "Ihre E-Mail-Adresse wurde bestätigt"
msgid "Your password has successfully been reset"
msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt"
-#: lib/mv_web/live/member_live/form.ex:52
-#: lib/mv_web/live/property_live/form.ex:44
-#: lib/mv_web/live/property_type_live/form.ex:32
-#: lib/mv_web/live/user_live/form.ex:95
+#: lib/mv_web/live/custom_field_live/form.ex:66
+#: lib/mv_web/live/custom_field_value_live/form.ex:77
+#: lib/mv_web/live/member_live/form.ex:82
+#: lib/mv_web/live/user_live/form.ex:127
#, elixir-autogen, elixir-format
msgid "Cancel"
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
msgid "Choose a member"
msgstr "Mitglied auswählen"
-#: lib/mv_web/live/property_live/form.ex:20
-#, elixir-autogen, elixir-format
-msgid "Choose a property type"
-msgstr "Eigenschaftstyp auswählen"
-
-#: lib/mv_web/live/property_type_live/form.ex:25
+#: lib/mv_web/live/custom_field_live/form.ex:59
#, elixir-autogen, elixir-format
msgid "Description"
msgstr "Beschreibung"
-#: lib/mv_web/live/user_live/show.ex:18
+#: lib/mv_web/live/user_live/show.ex:43
#, elixir-autogen, elixir-format
msgid "Edit User"
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
msgid "Enabled"
msgstr "Aktiviert"
-#: lib/mv_web/live/user_live/show.ex:24
+#: lib/mv_web/live/user_live/show.ex:49
#, elixir-autogen, elixir-format
msgid "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
msgid "Immutable"
msgstr "Unveränderlich"
-#: lib/mv_web/components/layouts/navbar.ex:93
+#: lib/mv_web/components/layouts/navbar.ex:94
#, elixir-autogen, elixir-format
msgid "Logout"
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
#, elixir-autogen, elixir-format
msgid "Listing Users"
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
msgid "Member"
msgstr "Mitglied"
#: 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
#, elixir-autogen, elixir-format
msgid "Members"
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
msgid "Name"
msgstr "Name"
@@ -334,73 +323,43 @@ msgstr "Name"
msgid "New User"
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
msgid "Not enabled"
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
msgid "Not set"
msgstr "Nicht gesetzt"
-#: lib/mv_web/live/user_live/form.ex:75
-#: lib/mv_web/live/user_live/form.ex:83
+#: lib/mv_web/live/user_live/form.ex:107
+#: lib/mv_web/live/user_live/form.ex:115
#, elixir-autogen, elixir-format
msgid "Note"
msgstr "Hinweis"
#: 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
msgid "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
msgid "Password Authentication"
msgstr "Passwort-Authentifizierung"
-#: lib/mv_web/live/property_live/form.ex:37
-#, 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
+#: lib/mv_web/components/layouts/navbar.ex:89
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr "Profil"
-#: lib/mv_web/live/property_live/form.ex:207
-#, 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
+#: lib/mv_web/live/custom_field_live/form.ex:61
#, elixir-autogen, elixir-format
msgid "Required"
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
#, elixir-autogen, elixir-format
msgid "Select all members"
@@ -411,58 +370,48 @@ msgstr "Alle Mitglieder auswählen"
msgid "Select member"
msgstr "Mitglied auswählen"
-#: lib/mv_web/components/layouts/navbar.ex:91
+#: lib/mv_web/components/layouts/navbar.ex:92
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr "Einstellungen"
-#: lib/mv_web/live/user_live/form.ex:93
+#: lib/mv_web/live/user_live/form.ex:125
#, elixir-autogen, elixir-format
msgid "Save User"
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
msgid "Show User"
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
msgid "This is a user record from your database."
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
msgid "Unsupported value type: %{type}"
msgstr "Nicht unterstützter Wertetyp: %{type}"
-#: lib/mv_web/live/property_live/form.ex:10
-#, 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
+#: lib/mv_web/live/user_live/form.ex:42
#, elixir-autogen, elixir-format
msgid "Use this form to manage user records in your database."
msgstr "Verwenden Sie dieses Formular, um Benutzer*innen-Datensätze zu verwalten."
-#: lib/mv_web/live/user_live/form.ex:110
-#: lib/mv_web/live/user_live/show.ex:9
+#: lib/mv_web/live/user_live/form.ex:142
+#: lib/mv_web/live/user_live/show.ex:34
#, elixir-autogen, elixir-format
msgid "User"
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
msgid "Value"
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
msgid "Value type"
msgstr "Wertetyp"
@@ -479,57 +428,57 @@ msgstr "aufsteigend"
msgid "descending"
msgstr "absteigend"
-#: lib/mv_web/live/user_live/form.ex:109
+#: lib/mv_web/live/user_live/form.ex:141
#, elixir-autogen, elixir-format
msgid "New"
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
msgid "Admin Note"
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
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."
-#: lib/mv_web/live/user_live/form.ex:55
+#: lib/mv_web/live/user_live/form.ex:87
#, elixir-autogen, elixir-format
msgid "At least 8 characters"
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
msgid "Change Password"
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
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."
-#: lib/mv_web/live/user_live/form.ex:45
+#: lib/mv_web/live/user_live/form.ex:77
#, elixir-autogen, elixir-format
msgid "Confirm Password"
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
msgid "Consider using special characters"
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
msgid "Include both letters and numbers"
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
msgid "Password"
msgstr "Passwort"
-#: lib/mv_web/live/user_live/form.ex:53
+#: lib/mv_web/live/user_live/form.ex:85
#, elixir-autogen, elixir-format
msgid "Password requirements"
msgstr "Passwort-Anforderungen"
@@ -544,56 +493,56 @@ msgstr "Alle Benutzer*innen auswählen"
msgid "Select user"
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
msgid "Set Password"
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
msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "Benutzer*in wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen', um eines hinzuzufügen."
-#: lib/mv_web/live/user_live/show.ex:30
+#: lib/mv_web/live/user_live/show.ex:55
#, elixir-autogen, elixir-format
msgid "Linked Member"
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
msgid "Linked User"
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
msgid "No member linked"
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
msgid "No user linked"
msgstr "Keine*r Benutzer*in verknüpft"
-#: lib/mv_web/live/member_live/show.ex:14
-#: lib/mv_web/live/member_live/show.ex:16
+#: lib/mv_web/live/member_live/show.ex:36
+#: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format
msgid "Back to members list"
msgstr "Zurück zur Mitgliederliste"
-#: lib/mv_web/live/user_live/show.ex:13
-#: lib/mv_web/live/user_live/show.ex:15
+#: lib/mv_web/live/user_live/show.ex:38
+#: lib/mv_web/live/user_live/show.ex:40
#, elixir-autogen, elixir-format
msgid "Back to users list"
msgstr "Zurück zur Benutzer*innen-Liste"
-#: lib/mv_web/components/layouts/navbar.ex:26
-#: lib/mv_web/components/layouts/navbar.ex:32
+#: lib/mv_web/components/layouts/navbar.ex:27
+#: lib/mv_web/components/layouts/navbar.ex:33
#, elixir-autogen, elixir-format
msgid "Select language"
msgstr "Sprache auswählen"
-#: lib/mv_web/components/layouts/navbar.ex:39
-#: lib/mv_web/components/layouts/navbar.ex:59
+#: lib/mv_web/components/layouts/navbar.ex:40
+#: lib/mv_web/components/layouts/navbar.ex:60
#, elixir-autogen, elixir-format
msgid "Toggle dark mode"
msgstr "Dunklen Modus umschalten"
@@ -604,7 +553,7 @@ msgstr "Dunklen Modus umschalten"
msgid "Search..."
msgstr "Suchen..."
-#: lib/mv_web/components/layouts/navbar.ex:20
+#: lib/mv_web/components/layouts/navbar.ex:21
#, elixir-autogen, elixir-format
msgid "Users"
msgstr "Benutzer*innen"
@@ -650,3 +599,59 @@ msgstr "E-Mail kann nicht aktualisiert werden: Diese E-Mail-Adresse ist bereits
#, elixir-autogen, elixir-format
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."
+
+#: 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"
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index ebcda96..d150a60 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -28,9 +28,9 @@ msgstr ""
msgid "Attempting to reconnect"
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/show.ex:37
+#: lib/mv_web/live/member_live/show.ex:59
#, elixir-autogen, elixir-format
msgid "City"
msgstr ""
@@ -42,43 +42,43 @@ msgid "Delete"
msgstr ""
#: 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
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:19
-#: lib/mv_web/live/member_live/show.ex:95
+#: lib/mv_web/live/member_live/show.ex:41
+#: lib/mv_web/live/member_live/show.ex:117
#, elixir-autogen, elixir-format
msgid "Edit Member"
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/show.ex:28
-#: lib/mv_web/live/user_live/form.ex:14
+#: lib/mv_web/live/member_live/show.ex:50
+#: lib/mv_web/live/user_live/form.ex:46
#: lib/mv_web/live/user_live/index.html.heex:44
-#: lib/mv_web/live/user_live/show.ex:25
+#: lib/mv_web/live/user_live/show.ex:50
#, elixir-autogen, elixir-format
msgid "Email"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:16
-#: lib/mv_web/live/member_live/show.ex:26
+#: lib/mv_web/live/member_live/form.ex:45
+#: lib/mv_web/live/member_live/show.ex:48
#, elixir-autogen, elixir-format
msgid "First Name"
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/show.ex:34
+#: lib/mv_web/live/member_live/show.ex:56
#, elixir-autogen, elixir-format
msgid "Join Date"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:17
-#: lib/mv_web/live/member_live/show.ex:27
+#: lib/mv_web/live/member_live/form.ex:46
+#: lib/mv_web/live/member_live/show.ex:49
#, elixir-autogen, elixir-format
msgid "Last Name"
msgstr ""
@@ -109,117 +109,111 @@ msgstr ""
msgid "close"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:19
-#: lib/mv_web/live/member_live/show.ex:29
+#: lib/mv_web/live/member_live/form.ex:48
+#: lib/mv_web/live/member_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "Birth Date"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:30
-#: lib/mv_web/live/member_live/show.ex:56
-#, 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
+#: lib/mv_web/live/member_live/form.ex:52
+#: lib/mv_web/live/member_live/show.ex:57
#, elixir-autogen, elixir-format
msgid "Exit Date"
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/show.ex:39
+#: lib/mv_web/live/member_live/show.ex:61
#, elixir-autogen, elixir-format
msgid "House Number"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:24
-#: lib/mv_web/live/member_live/show.ex:36
+#: lib/mv_web/live/member_live/form.ex:53
+#: lib/mv_web/live/member_live/show.ex:58
#, elixir-autogen, elixir-format
msgid "Notes"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:20
-#: lib/mv_web/live/member_live/show.ex:30
+#: lib/mv_web/live/member_live/form.ex:49
+#: lib/mv_web/live/member_live/show.ex:52
#, elixir-autogen, elixir-format
msgid "Paid"
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/show.ex:33
+#: lib/mv_web/live/member_live/show.ex:55
#, elixir-autogen, elixir-format
msgid "Phone Number"
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/show.ex:40
+#: lib/mv_web/live/member_live/show.ex:62
#, elixir-autogen, elixir-format
msgid "Postal Code"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:50
+#: lib/mv_web/live/member_live/form.ex:80
#, elixir-autogen, elixir-format
msgid "Save Member"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:49
-#: lib/mv_web/live/property_live/form.ex:41
-#: lib/mv_web/live/property_type_live/form.ex:29
-#: lib/mv_web/live/user_live/form.ex:92
+#: lib/mv_web/live/custom_field_live/form.ex:63
+#: lib/mv_web/live/custom_field_value_live/form.ex:74
+#: lib/mv_web/live/member_live/form.ex:79
+#: lib/mv_web/live/user_live/form.ex:124
#, elixir-autogen, elixir-format
msgid "Saving..."
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/show.ex:38
+#: lib/mv_web/live/member_live/show.ex:60
#, elixir-autogen, elixir-format
msgid "Street"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:11
+#: lib/mv_web/live/member_live/form.ex:40
#, elixir-autogen, elixir-format
msgid "Use this form to manage member records and their properties."
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:25
+#: lib/mv_web/live/member_live/show.ex:47
#, elixir-autogen, elixir-format
msgid "Id"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:31
+#: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "No"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:94
+#: lib/mv_web/live/member_live/show.ex:116
#, elixir-autogen, elixir-format
msgid "Show Member"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:11
+#: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format
msgid "This is a member record from your database."
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:31
+#: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "Yes"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:108
-#: lib/mv_web/live/property_live/form.ex:200
-#: lib/mv_web/live/property_type_live/form.ex:73
+#: lib/mv_web/live/custom_field_live/form.ex:107
+#: lib/mv_web/live/custom_field_value_live/form.ex:233
+#: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format
msgid "create"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:109
-#: lib/mv_web/live/property_live/form.ex:201
-#: lib/mv_web/live/property_type_live/form.ex:74
+#: lib/mv_web/live/custom_field_live/form.ex:108
+#: lib/mv_web/live/custom_field_value_live/form.ex:234
+#: lib/mv_web/live/member_live/form.ex:139
#, elixir-autogen, elixir-format
msgid "update"
msgstr ""
@@ -229,7 +223,7 @@ msgstr ""
msgid "Incorrect email or password"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:115
+#: lib/mv_web/live/member_live/form.ex:145
#, elixir-autogen, elixir-format
msgid "Member %{action} successfully"
msgstr ""
@@ -259,73 +253,68 @@ msgstr ""
msgid "Your password has successfully been reset"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:52
-#: lib/mv_web/live/property_live/form.ex:44
-#: lib/mv_web/live/property_type_live/form.ex:32
-#: lib/mv_web/live/user_live/form.ex:95
+#: lib/mv_web/live/custom_field_live/form.ex:66
+#: lib/mv_web/live/custom_field_value_live/form.ex:77
+#: lib/mv_web/live/member_live/form.ex:82
+#: lib/mv_web/live/user_live/form.ex:127
#, elixir-autogen, elixir-format
msgid "Cancel"
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
msgid "Choose a member"
msgstr ""
-#: lib/mv_web/live/property_live/form.ex:20
-#, elixir-autogen, elixir-format
-msgid "Choose a property type"
-msgstr ""
-
-#: lib/mv_web/live/property_type_live/form.ex:25
+#: lib/mv_web/live/custom_field_live/form.ex:59
#, elixir-autogen, elixir-format
msgid "Description"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:18
+#: lib/mv_web/live/user_live/show.ex:43
#, elixir-autogen, elixir-format
msgid "Edit User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:28
+#: lib/mv_web/live/user_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "Enabled"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:24
+#: lib/mv_web/live/user_live/show.ex:49
#, elixir-autogen, elixir-format
msgid "ID"
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
msgid "Immutable"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:93
+#: lib/mv_web/components/layouts/navbar.ex:94
#, elixir-autogen, elixir-format
msgid "Logout"
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
#, elixir-autogen, elixir-format
msgid "Listing Users"
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
msgid "Member"
msgstr ""
#: 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
#, elixir-autogen, elixir-format
msgid "Members"
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
msgid "Name"
msgstr ""
@@ -335,73 +324,43 @@ msgstr ""
msgid "New User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:28
+#: lib/mv_web/live/user_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "Not enabled"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:26
+#: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "Not set"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:75
-#: lib/mv_web/live/user_live/form.ex:83
+#: lib/mv_web/live/user_live/form.ex:107
+#: lib/mv_web/live/user_live/form.ex:115
#, elixir-autogen, elixir-format
msgid "Note"
msgstr ""
#: 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
msgid "OIDC ID"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:27
+#: lib/mv_web/live/user_live/show.ex:52
#, elixir-autogen, elixir-format
msgid "Password Authentication"
msgstr ""
-#: lib/mv_web/live/property_live/form.ex:37
-#, elixir-autogen, elixir-format
-msgid "Please select a property type first"
-msgstr ""
-
-#: lib/mv_web/components/layouts/navbar.ex:88
+#: lib/mv_web/components/layouts/navbar.ex:89
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr ""
-#: lib/mv_web/live/property_live/form.ex:207
-#, 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
+#: lib/mv_web/live/custom_field_live/form.ex:61
#, elixir-autogen, elixir-format
msgid "Required"
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
#, elixir-autogen, elixir-format
msgid "Select all members"
@@ -412,58 +371,48 @@ msgstr ""
msgid "Select member"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:91
+#: lib/mv_web/components/layouts/navbar.ex:92
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:93
+#: lib/mv_web/live/user_live/form.ex:125
#, elixir-autogen, elixir-format
msgid "Save User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:54
+#: lib/mv_web/live/user_live/show.ex:79
#, elixir-autogen, elixir-format
msgid "Show User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:10
+#: lib/mv_web/live/user_live/show.ex:35
#, elixir-autogen, elixir-format
msgid "This is a user record from your database."
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
msgid "Unsupported value type: %{type}"
msgstr ""
-#: lib/mv_web/live/property_live/form.ex:10
-#, 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
+#: lib/mv_web/live/user_live/form.ex:42
#, elixir-autogen, elixir-format
msgid "Use this form to manage user records in your database."
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:110
-#: lib/mv_web/live/user_live/show.ex:9
+#: lib/mv_web/live/user_live/form.ex:142
+#: lib/mv_web/live/user_live/show.ex:34
#, elixir-autogen, elixir-format
msgid "User"
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
msgid "Value"
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
msgid "Value type"
msgstr ""
@@ -480,57 +429,57 @@ msgstr ""
msgid "descending"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:109
+#: lib/mv_web/live/user_live/form.ex:141
#, elixir-autogen, elixir-format
msgid "New"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:64
+#: lib/mv_web/live/user_live/form.ex:96
#, elixir-autogen, elixir-format
msgid "Admin Note"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:64
+#: lib/mv_web/live/user_live/form.ex:96
#, 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."
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:55
+#: lib/mv_web/live/user_live/form.ex:87
#, elixir-autogen, elixir-format
msgid "At least 8 characters"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:27
+#: lib/mv_web/live/user_live/form.ex:59
#, elixir-autogen, elixir-format
msgid "Change Password"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:75
+#: lib/mv_web/live/user_live/form.ex:107
#, elixir-autogen, elixir-format
msgid "Check 'Change Password' above to set a new password for this user."
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:45
+#: lib/mv_web/live/user_live/form.ex:77
#, elixir-autogen, elixir-format
msgid "Confirm Password"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:57
+#: lib/mv_web/live/user_live/form.ex:89
#, elixir-autogen, elixir-format
msgid "Consider using special characters"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:56
+#: lib/mv_web/live/user_live/form.ex:88
#, elixir-autogen, elixir-format
msgid "Include both letters and numbers"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:35
+#: lib/mv_web/live/user_live/form.ex:67
#, elixir-autogen, elixir-format
msgid "Password"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:53
+#: lib/mv_web/live/user_live/form.ex:85
#, elixir-autogen, elixir-format
msgid "Password requirements"
msgstr ""
@@ -545,56 +494,56 @@ msgstr ""
msgid "Select user"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:27
+#: lib/mv_web/live/user_live/form.ex:59
#, elixir-autogen, elixir-format
msgid "Set Password"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:83
+#: lib/mv_web/live/user_live/form.ex:115
#, elixir-autogen, elixir-format
msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:30
+#: lib/mv_web/live/user_live/show.ex:55
#, elixir-autogen, elixir-format
msgid "Linked Member"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:41
+#: lib/mv_web/live/member_live/show.ex:63
#, elixir-autogen, elixir-format
msgid "Linked User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:40
+#: lib/mv_web/live/user_live/show.ex:65
#, elixir-autogen, elixir-format
msgid "No member linked"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:51
+#: lib/mv_web/live/member_live/show.ex:73
#, elixir-autogen, elixir-format
msgid "No user linked"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:14
-#: lib/mv_web/live/member_live/show.ex:16
+#: lib/mv_web/live/member_live/show.ex:36
+#: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format
msgid "Back to members list"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:13
-#: lib/mv_web/live/user_live/show.ex:15
+#: lib/mv_web/live/user_live/show.ex:38
+#: lib/mv_web/live/user_live/show.ex:40
#, elixir-autogen, elixir-format
msgid "Back to users list"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:26
-#: lib/mv_web/components/layouts/navbar.ex:32
+#: lib/mv_web/components/layouts/navbar.ex:27
+#: lib/mv_web/components/layouts/navbar.ex:33
#, elixir-autogen, elixir-format
msgid "Select language"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:39
-#: lib/mv_web/components/layouts/navbar.ex:59
+#: lib/mv_web/components/layouts/navbar.ex:40
+#: lib/mv_web/components/layouts/navbar.ex:60
#, elixir-autogen, elixir-format
msgid "Toggle dark mode"
msgstr ""
@@ -605,7 +554,7 @@ msgstr ""
msgid "Search..."
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:20
+#: lib/mv_web/components/layouts/navbar.ex:21
#, elixir-autogen, elixir-format
msgid "Users"
msgstr ""
@@ -651,3 +600,59 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
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 ""
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index bc0e16c..df56e75 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -28,9 +28,9 @@ msgstr ""
msgid "Attempting to reconnect"
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/show.ex:37
+#: lib/mv_web/live/member_live/show.ex:59
#, elixir-autogen, elixir-format
msgid "City"
msgstr ""
@@ -42,43 +42,43 @@ msgid "Delete"
msgstr ""
#: 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
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:19
-#: lib/mv_web/live/member_live/show.ex:95
+#: lib/mv_web/live/member_live/show.ex:41
+#: lib/mv_web/live/member_live/show.ex:117
#, elixir-autogen, elixir-format
msgid "Edit Member"
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/show.ex:28
-#: lib/mv_web/live/user_live/form.ex:14
+#: lib/mv_web/live/member_live/show.ex:50
+#: lib/mv_web/live/user_live/form.ex:46
#: lib/mv_web/live/user_live/index.html.heex:44
-#: lib/mv_web/live/user_live/show.ex:25
+#: lib/mv_web/live/user_live/show.ex:50
#, elixir-autogen, elixir-format
msgid "Email"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:16
-#: lib/mv_web/live/member_live/show.ex:26
+#: lib/mv_web/live/member_live/form.ex:45
+#: lib/mv_web/live/member_live/show.ex:48
#, elixir-autogen, elixir-format
msgid "First Name"
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/show.ex:34
+#: lib/mv_web/live/member_live/show.ex:56
#, elixir-autogen, elixir-format
msgid "Join Date"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:17
-#: lib/mv_web/live/member_live/show.ex:27
+#: lib/mv_web/live/member_live/form.ex:46
+#: lib/mv_web/live/member_live/show.ex:49
#, elixir-autogen, elixir-format
msgid "Last Name"
msgstr ""
@@ -109,117 +109,111 @@ msgstr ""
msgid "close"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:19
-#: lib/mv_web/live/member_live/show.ex:29
+#: lib/mv_web/live/member_live/form.ex:48
+#: lib/mv_web/live/member_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "Birth Date"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:30
-#: lib/mv_web/live/member_live/show.ex:56
-#, 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
+#: lib/mv_web/live/member_live/form.ex:52
+#: lib/mv_web/live/member_live/show.ex:57
#, elixir-autogen, elixir-format
msgid "Exit Date"
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/show.ex:39
+#: lib/mv_web/live/member_live/show.ex:61
#, elixir-autogen, elixir-format
msgid "House Number"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:24
-#: lib/mv_web/live/member_live/show.ex:36
+#: lib/mv_web/live/member_live/form.ex:53
+#: lib/mv_web/live/member_live/show.ex:58
#, elixir-autogen, elixir-format
msgid "Notes"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:20
-#: lib/mv_web/live/member_live/show.ex:30
+#: lib/mv_web/live/member_live/form.ex:49
+#: lib/mv_web/live/member_live/show.ex:52
#, elixir-autogen, elixir-format
msgid "Paid"
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/show.ex:33
+#: lib/mv_web/live/member_live/show.ex:55
#, elixir-autogen, elixir-format
msgid "Phone Number"
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/show.ex:40
+#: lib/mv_web/live/member_live/show.ex:62
#, elixir-autogen, elixir-format
msgid "Postal Code"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:50
+#: lib/mv_web/live/member_live/form.ex:80
#, elixir-autogen, elixir-format, fuzzy
msgid "Save Member"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:49
-#: lib/mv_web/live/property_live/form.ex:41
-#: lib/mv_web/live/property_type_live/form.ex:29
-#: lib/mv_web/live/user_live/form.ex:92
+#: lib/mv_web/live/custom_field_live/form.ex:63
+#: lib/mv_web/live/custom_field_value_live/form.ex:74
+#: lib/mv_web/live/member_live/form.ex:79
+#: lib/mv_web/live/user_live/form.ex:124
#, elixir-autogen, elixir-format
msgid "Saving..."
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/show.ex:38
+#: lib/mv_web/live/member_live/show.ex:60
#, elixir-autogen, elixir-format
msgid "Street"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:11
+#: lib/mv_web/live/member_live/form.ex:40
#, elixir-autogen, elixir-format
msgid "Use this form to manage member records and their properties."
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:25
+#: lib/mv_web/live/member_live/show.ex:47
#, elixir-autogen, elixir-format
msgid "Id"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:31
+#: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "No"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:94
+#: lib/mv_web/live/member_live/show.ex:116
#, elixir-autogen, elixir-format, fuzzy
msgid "Show Member"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:11
+#: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format
msgid "This is a member record from your database."
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:31
+#: lib/mv_web/live/member_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "Yes"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:108
-#: lib/mv_web/live/property_live/form.ex:200
-#: lib/mv_web/live/property_type_live/form.ex:73
+#: lib/mv_web/live/custom_field_live/form.ex:107
+#: lib/mv_web/live/custom_field_value_live/form.ex:233
+#: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format
msgid "create"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:109
-#: lib/mv_web/live/property_live/form.ex:201
-#: lib/mv_web/live/property_type_live/form.ex:74
+#: lib/mv_web/live/custom_field_live/form.ex:108
+#: lib/mv_web/live/custom_field_value_live/form.ex:234
+#: lib/mv_web/live/member_live/form.ex:139
#, elixir-autogen, elixir-format
msgid "update"
msgstr ""
@@ -229,7 +223,7 @@ msgstr ""
msgid "Incorrect email or password"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:115
+#: lib/mv_web/live/member_live/form.ex:145
#, elixir-autogen, elixir-format
msgid "Member %{action} successfully"
msgstr ""
@@ -259,73 +253,68 @@ msgstr ""
msgid "Your password has successfully been reset"
msgstr ""
-#: lib/mv_web/live/member_live/form.ex:52
-#: lib/mv_web/live/property_live/form.ex:44
-#: lib/mv_web/live/property_type_live/form.ex:32
-#: lib/mv_web/live/user_live/form.ex:95
+#: lib/mv_web/live/custom_field_live/form.ex:66
+#: lib/mv_web/live/custom_field_value_live/form.ex:77
+#: lib/mv_web/live/member_live/form.ex:82
+#: lib/mv_web/live/user_live/form.ex:127
#, elixir-autogen, elixir-format
msgid "Cancel"
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
msgid "Choose a member"
msgstr ""
-#: lib/mv_web/live/property_live/form.ex:20
-#, elixir-autogen, elixir-format
-msgid "Choose a property type"
-msgstr ""
-
-#: lib/mv_web/live/property_type_live/form.ex:25
+#: lib/mv_web/live/custom_field_live/form.ex:59
#, elixir-autogen, elixir-format
msgid "Description"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:18
+#: lib/mv_web/live/user_live/show.ex:43
#, elixir-autogen, elixir-format, fuzzy
msgid "Edit User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:28
+#: lib/mv_web/live/user_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "Enabled"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:24
+#: lib/mv_web/live/user_live/show.ex:49
#, elixir-autogen, elixir-format
msgid "ID"
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
msgid "Immutable"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:93
+#: lib/mv_web/components/layouts/navbar.ex:94
#, elixir-autogen, elixir-format
msgid "Logout"
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
#, elixir-autogen, elixir-format, fuzzy
msgid "Listing Users"
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
msgid "Member"
msgstr ""
#: 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
#, elixir-autogen, elixir-format
msgid "Members"
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
msgid "Name"
msgstr ""
@@ -335,73 +324,43 @@ msgstr ""
msgid "New User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:28
+#: lib/mv_web/live/user_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "Not enabled"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:26
+#: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format, fuzzy
msgid "Not set"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:75
-#: lib/mv_web/live/user_live/form.ex:83
+#: lib/mv_web/live/user_live/form.ex:107
+#: lib/mv_web/live/user_live/form.ex:115
#, elixir-autogen, elixir-format, fuzzy
msgid "Note"
msgstr ""
#: 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
msgid "OIDC ID"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:27
+#: lib/mv_web/live/user_live/show.ex:52
#, elixir-autogen, elixir-format
msgid "Password Authentication"
msgstr ""
-#: lib/mv_web/live/property_live/form.ex:37
-#, elixir-autogen, elixir-format
-msgid "Please select a property type first"
-msgstr ""
-
-#: lib/mv_web/components/layouts/navbar.ex:88
+#: lib/mv_web/components/layouts/navbar.ex:89
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr ""
-#: lib/mv_web/live/property_live/form.ex:207
-#, 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
+#: lib/mv_web/live/custom_field_live/form.ex:61
#, elixir-autogen, elixir-format
msgid "Required"
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
#, elixir-autogen, elixir-format
msgid "Select all members"
@@ -412,58 +371,48 @@ msgstr ""
msgid "Select member"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:91
+#: lib/mv_web/components/layouts/navbar.ex:92
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:93
+#: lib/mv_web/live/user_live/form.ex:125
#, elixir-autogen, elixir-format, fuzzy
msgid "Save User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:54
+#: lib/mv_web/live/user_live/show.ex:79
#, elixir-autogen, elixir-format, fuzzy
msgid "Show User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:10
+#: lib/mv_web/live/user_live/show.ex:35
#, elixir-autogen, elixir-format, fuzzy
msgid "This is a user record from your database."
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
msgid "Unsupported value type: %{type}"
msgstr ""
-#: lib/mv_web/live/property_live/form.ex:10
-#, 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
+#: lib/mv_web/live/user_live/form.ex:42
#, elixir-autogen, elixir-format, fuzzy
msgid "Use this form to manage user records in your database."
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:110
-#: lib/mv_web/live/user_live/show.ex:9
+#: lib/mv_web/live/user_live/form.ex:142
+#: lib/mv_web/live/user_live/show.ex:34
#, elixir-autogen, elixir-format
msgid "User"
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
msgid "Value"
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
msgid "Value type"
msgstr ""
@@ -480,57 +429,57 @@ msgstr ""
msgid "descending"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:109
+#: lib/mv_web/live/user_live/form.ex:141
#, elixir-autogen, elixir-format
msgid "New"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:64
+#: lib/mv_web/live/user_live/form.ex:96
#, elixir-autogen, elixir-format
msgid "Admin Note"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:64
+#: lib/mv_web/live/user_live/form.ex:96
#, 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."
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
msgid "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
msgid "Change Password"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:75
+#: lib/mv_web/live/user_live/form.ex:107
#, elixir-autogen, elixir-format
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."
-#: lib/mv_web/live/user_live/form.ex:45
+#: lib/mv_web/live/user_live/form.ex:77
#, elixir-autogen, elixir-format
msgid "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
msgid "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
msgid "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
msgid "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
msgid "Password requirements"
msgstr "Password requirements"
@@ -545,56 +494,56 @@ msgstr ""
msgid "Select user"
msgstr ""
-#: lib/mv_web/live/user_live/form.ex:27
+#: lib/mv_web/live/user_live/form.ex:59
#, elixir-autogen, elixir-format
msgid "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
msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "User will be created without a password. Check 'Set Password' to add one."
-#: lib/mv_web/live/user_live/show.ex:30
+#: lib/mv_web/live/user_live/show.ex:55
#, elixir-autogen, elixir-format, fuzzy
msgid "Linked Member"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:41
+#: lib/mv_web/live/member_live/show.ex:63
#, elixir-autogen, elixir-format
msgid "Linked User"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:40
+#: lib/mv_web/live/user_live/show.ex:65
#, elixir-autogen, elixir-format
msgid "No member linked"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:51
+#: lib/mv_web/live/member_live/show.ex:73
#, elixir-autogen, elixir-format
msgid "No user linked"
msgstr ""
-#: lib/mv_web/live/member_live/show.ex:14
-#: lib/mv_web/live/member_live/show.ex:16
+#: lib/mv_web/live/member_live/show.ex:36
+#: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format
msgid "Back to members list"
msgstr ""
-#: lib/mv_web/live/user_live/show.ex:13
-#: lib/mv_web/live/user_live/show.ex:15
+#: lib/mv_web/live/user_live/show.ex:38
+#: lib/mv_web/live/user_live/show.ex:40
#, elixir-autogen, elixir-format
msgid "Back to users list"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:26
-#: lib/mv_web/components/layouts/navbar.ex:32
+#: lib/mv_web/components/layouts/navbar.ex:27
+#: lib/mv_web/components/layouts/navbar.ex:33
#, elixir-autogen, elixir-format, fuzzy
msgid "Select language"
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:39
-#: lib/mv_web/components/layouts/navbar.ex:59
+#: lib/mv_web/components/layouts/navbar.ex:40
+#: lib/mv_web/components/layouts/navbar.ex:60
#, elixir-autogen, elixir-format
msgid "Toggle dark mode"
msgstr ""
@@ -605,7 +554,7 @@ msgstr ""
msgid "Search..."
msgstr ""
-#: lib/mv_web/components/layouts/navbar.ex:20
+#: lib/mv_web/components/layouts/navbar.ex:21
#, elixir-autogen, elixir-format, fuzzy
msgid "Users"
msgstr ""
@@ -651,3 +600,59 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
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 ""
diff --git a/priv/repo/migrations/20251113163600_rename_properties_to_custom_fields_extensions_1.exs b/priv/repo/migrations/20251113163600_rename_properties_to_custom_fields_extensions_1.exs
new file mode 100644
index 0000000..2fafbd3
--- /dev/null
+++ b/priv/repo/migrations/20251113163600_rename_properties_to_custom_fields_extensions_1.exs
@@ -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
diff --git a/priv/repo/migrations/20251113163602_rename_properties_to_custom_fields.exs b/priv/repo/migrations/20251113163602_rename_properties_to_custom_fields.exs
new file mode 100644
index 0000000..0517c0b
--- /dev/null
+++ b/priv/repo/migrations/20251113163602_rename_properties_to_custom_fields.exs
@@ -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
diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs
index a0299fd..8d3cb6f 100644
--- a/priv/repo/seeds.exs
+++ b/priv/repo/seeds.exs
@@ -7,36 +7,94 @@ alias Mv.Membership
alias Mv.Accounts
for attrs <- [
+ # Basic example fields (for testing)
%{
name: "String Field",
value_type: :string,
description: "Example for a field of type string",
immutable: true,
- required: true
+ required: false
},
%{
name: "Date Field",
value_type: :date,
description: "Example for a field of type date",
immutable: true,
- required: true
+ required: false
},
%{
name: "Boolean Field",
value_type: :boolean,
description: "Example for a field of type boolean",
immutable: true,
- required: true
+ required: false
},
%{
name: "Email Field",
value_type: :email,
description: "Example for a field of type email",
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
- Membership.create_property_type!(
+ Membership.create_custom_field!(
attrs,
upsert?: true,
upsert_identity: :unique_name
@@ -180,9 +238,94 @@ Enum.each(linked_members, fn member_attrs ->
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("📝 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(" - 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"
)
-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!")
diff --git a/priv/resource_snapshots/repo/custom_field_values/20251113163602.json b/priv/resource_snapshots/repo/custom_field_values/20251113163602.json
new file mode 100644
index 0000000..2069939
--- /dev/null
+++ b/priv/resource_snapshots/repo/custom_field_values/20251113163602.json
@@ -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"
+}
\ No newline at end of file
diff --git a/priv/resource_snapshots/repo/custom_fields/20251113163602.json b/priv/resource_snapshots/repo/custom_fields/20251113163602.json
new file mode 100644
index 0000000..f3959cb
--- /dev/null
+++ b/priv/resource_snapshots/repo/custom_fields/20251113163602.json
@@ -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"
+}
\ No newline at end of file
diff --git a/priv/resource_snapshots/repo/extensions.json b/priv/resource_snapshots/repo/extensions.json
index 323661b..3731105 100644
--- a/priv/resource_snapshots/repo/extensions.json
+++ b/priv/resource_snapshots/repo/extensions.json
@@ -2,6 +2,7 @@
"ash_functions_version": 5,
"installed": [
"ash-functions",
- "citext"
+ "citext",
+ "pg_trgm"
]
}
\ No newline at end of file
diff --git a/test/membership/custom_field_validation_test.exs b/test/membership/custom_field_validation_test.exs
new file mode 100644
index 0000000..a5c1f2d
--- /dev/null
+++ b/test/membership/custom_field_validation_test.exs
@@ -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
diff --git a/test/membership/custom_field_value_validation_test.exs b/test/membership/custom_field_value_validation_test.exs
new file mode 100644
index 0000000..dd3438a
--- /dev/null
+++ b/test/membership/custom_field_value_validation_test.exs
@@ -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
diff --git a/test/mv_web/live/profile_navigation_test.exs b/test/mv_web/live/profile_navigation_test.exs
index 8a59656..3222825 100644
--- a/test/mv_web/live/profile_navigation_test.exs
+++ b/test/mv_web/live/profile_navigation_test.exs
@@ -148,10 +148,10 @@ defmodule MvWeb.ProfileNavigationTest do
"/",
"/members",
"/members/new",
- "/properties",
- "/properties/new",
- "/property_types",
- "/property_types/new",
+ "/custom_field_values",
+ "/custom_field_values/new",
+ "/custom_fields",
+ "/custom_fields/new",
"/users",
"/users/new"
]
diff --git a/test/seeds_test.exs b/test/seeds_test.exs
index 5c589ae..6d29760 100644
--- a/test/seeds_test.exs
+++ b/test/seeds_test.exs
@@ -9,11 +9,11 @@ defmodule Mv.SeedsTest do
# Basic smoke test: ensure some data was created
{:ok, users} = Ash.read(Mv.Accounts.User)
{: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(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
test "can be run multiple times (idempotent)" do
@@ -23,7 +23,7 @@ defmodule Mv.SeedsTest do
# Count records
{:ok, users_count_1} = Ash.read(Mv.Accounts.User)
{: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
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)
{:ok, users_count_2} = Ash.read(Mv.Accounts.User)
{: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),
"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),
"Members count should remain same after re-running seeds"
- assert length(property_types_count_1) == length(property_types_count_2),
- "PropertyTypes count should remain same after re-running seeds"
+ assert length(custom_fields_count_1) == length(custom_fields_count_2),
+ "CustomFields count should remain same after re-running seeds"
end
end
end