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/property_type.ex b/lib/membership/custom_field.ex
similarity index 58%
rename from lib/membership/property_type.ex
rename to lib/membership/custom_field.ex
index 6569d1b..d7323ff 100644
--- a/lib/membership/property_type.ex
+++ b/lib/membership/custom_field.ex
@@ -1,18 +1,18 @@
-defmodule Mv.Membership.PropertyType do
+defmodule Mv.Membership.CustomField do
@moduledoc """
- Ash resource defining the schema for custom member properties.
+ Ash resource defining the schema for custom member fields.
## 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.
+ 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 property (e.g., "phone_mobile", "birthday")
+ - `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, property values cannot be changed after creation
- - `required` - If true, all members must have this property (future feature)
+ - `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 (unlimited length)
@@ -22,22 +22,22 @@ defmodule Mv.Membership.PropertyType do
- `:email` - Validated email addresses
## Relationships
- - `has_many :properties` - All property values of this type
+ - `has_many :custom_field_values` - All custom field values of this type
## Constraints
- - Name must be unique across all property types
- - Cannot delete a property type that has existing property values (RESTRICT)
+ - Name must be unique across all custom fields
+ - Cannot delete a custom field that has existing custom field values (RESTRICT)
## Examples
- # Create a new property type
- PropertyType.create!(%{
+ # Create a new custom field
+ CustomField.create!(%{
name: "phone_mobile",
value_type: :string,
description: "Mobile phone number"
})
- # Create a required property type
- PropertyType.create!(%{
+ # Create a required custom field
+ CustomField.create!(%{
name: "emergency_contact",
value_type: :string,
required: true
@@ -48,7 +48,7 @@ defmodule Mv.Membership.PropertyType do
data_layer: AshPostgres.DataLayer
postgres do
- table "property_types"
+ table "custom_fields"
repo Mv.Repo
end
@@ -65,7 +65,7 @@ defmodule Mv.Membership.PropertyType do
attribute :value_type, :atom,
constraints: [one_of: [:string, :integer, :boolean, :date, :email]],
allow_nil?: false,
- description: "Defines the datatype `Property.value` is interpreted as"
+ description: "Defines the datatype `CustomFieldValue.value` is interpreted as"
attribute :description, :string, allow_nil?: true, public?: true
@@ -79,10 +79,11 @@ defmodule Mv.Membership.PropertyType do
end
relationships do
- has_many :properties, Mv.Membership.Property
+ has_many :custom_field_values, Mv.Membership.CustomFieldValue
end
identities do
identity :unique_name, [:name]
end
end
+
diff --git a/lib/membership/property.ex b/lib/membership/custom_field_value.ex
similarity index 53%
rename from lib/membership/property.ex
rename to lib/membership/custom_field_value.ex
index 231b264..908f331 100644
--- a/lib/membership/property.ex
+++ b/lib/membership/custom_field_value.ex
@@ -1,11 +1,11 @@
-defmodule Mv.Membership.Property do
+defmodule Mv.Membership.CustomFieldValue do
@moduledoc """
- Ash resource representing a custom property value for a member.
+ Ash resource representing a custom field 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.
+ 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:
@@ -24,19 +24,19 @@ defmodule Mv.Membership.Property do
- `: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
+ - `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 property per property type (unique composite index)
- - Properties are deleted when the associated member is deleted (CASCADE)
+ - 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)
"""
use Ash.Resource,
domain: Mv.Membership,
data_layer: AshPostgres.DataLayer
postgres do
- table "properties"
+ table "custom_field_values"
repo Mv.Repo
references do
@@ -46,7 +46,7 @@ defmodule Mv.Membership.Property do
actions do
defaults [:create, :read, :update, :destroy]
- default_accept [:value, :member_id, :property_type_id]
+ default_accept [:value, :member_id, :custom_field_id]
end
attributes do
@@ -68,16 +68,17 @@ defmodule Mv.Membership.Property do
relationships do
belongs_to :member, Mv.Membership.Member
- belongs_to :property_type, Mv.Membership.PropertyType
+ 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 property per property type
- # For example: A member can have only one "email" property, one "phone" property, etc.
+ # 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_property_per_member, [:member_id, :property_type_id]
+ 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..47651f5 100644
--- a/lib/membership/email.ex
+++ b/lib/membership/email.ex
@@ -4,7 +4,7 @@ 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
@@ -14,12 +14,12 @@ defmodule Mv.Membership.Email do
- Automatic trimming of leading/trailing whitespace
## 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
})
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/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..1772bf7 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,18 @@ 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..f0bb290
--- /dev/null
+++ b/lib/mv_web/live/custom_field_live/index.ex
@@ -0,0 +1,89 @@
+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"""
+