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