Merge pull request 'Custom Fields: Harden implementation closes #194' (#204) from feature/harden-custom-fields into main
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #204 Reviewed-by: carla <carla@noreply.git.local-it.org>
This commit is contained in:
commit
a273b54c75
39 changed files with 2337 additions and 1269 deletions
|
|
@ -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.
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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}` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
101
lib/membership/custom_field.ex
Normal file
101
lib/membership/custom_field.ex
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
defmodule Mv.Membership.CustomField do
|
||||
@moduledoc """
|
||||
Ash resource defining the schema for custom member fields.
|
||||
|
||||
## Overview
|
||||
CustomFields define the "schema" for custom fields in the membership system.
|
||||
Each CustomField specifies the name, data type, and behavior of a custom field
|
||||
that can be attached to members via CustomFieldValue resources.
|
||||
|
||||
## Attributes
|
||||
- `name` - Unique identifier for the custom field (e.g., "phone_mobile", "birthday")
|
||||
- `value_type` - Data type constraint (`:string`, `:integer`, `:boolean`, `:date`, `:email`)
|
||||
- `description` - Optional human-readable description
|
||||
- `immutable` - If true, custom field values cannot be changed after creation
|
||||
- `required` - If true, all members must have this custom field (future feature)
|
||||
|
||||
## Supported Value Types
|
||||
- `:string` - Text data (max 10,000 characters)
|
||||
- `:integer` - Numeric data (64-bit integers)
|
||||
- `:boolean` - True/false flags
|
||||
- `:date` - Date values (no time component)
|
||||
- `:email` - Validated email addresses (max 254 characters)
|
||||
|
||||
## Relationships
|
||||
- `has_many :custom_field_values` - All custom field values of this type
|
||||
|
||||
## Constraints
|
||||
- Name must be unique across all custom fields
|
||||
- Name maximum length: 100 characters
|
||||
- Cannot delete a custom field that has existing custom field values (RESTRICT)
|
||||
|
||||
## Examples
|
||||
# Create a new custom field
|
||||
CustomField.create!(%{
|
||||
name: "phone_mobile",
|
||||
value_type: :string,
|
||||
description: "Mobile phone number"
|
||||
})
|
||||
|
||||
# Create a required custom field
|
||||
CustomField.create!(%{
|
||||
name: "emergency_contact",
|
||||
value_type: :string,
|
||||
required: true
|
||||
})
|
||||
"""
|
||||
use Ash.Resource,
|
||||
domain: Mv.Membership,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
postgres do
|
||||
table "custom_fields"
|
||||
repo Mv.Repo
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
default_accept [:name, :value_type, :description, :immutable, :required]
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :name, :string,
|
||||
allow_nil?: false,
|
||||
public?: true,
|
||||
constraints: [
|
||||
max_length: 100,
|
||||
trim?: true
|
||||
]
|
||||
|
||||
attribute :value_type, :atom,
|
||||
constraints: [one_of: [:string, :integer, :boolean, :date, :email]],
|
||||
allow_nil?: false,
|
||||
description: "Defines the datatype `CustomFieldValue.value` is interpreted as"
|
||||
|
||||
attribute :description, :string,
|
||||
allow_nil?: true,
|
||||
public?: true,
|
||||
constraints: [
|
||||
max_length: 500,
|
||||
trim?: true
|
||||
]
|
||||
|
||||
attribute :immutable, :boolean,
|
||||
default: false,
|
||||
allow_nil?: false
|
||||
|
||||
attribute :required, :boolean,
|
||||
default: false,
|
||||
allow_nil?: false
|
||||
end
|
||||
|
||||
relationships do
|
||||
has_many :custom_field_values, Mv.Membership.CustomFieldValue
|
||||
end
|
||||
|
||||
identities do
|
||||
identity :unique_name, [:name]
|
||||
end
|
||||
end
|
||||
102
lib/membership/custom_field_value.ex
Normal file
102
lib/membership/custom_field_value.ex
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
defmodule Mv.Membership.CustomFieldValue do
|
||||
@moduledoc """
|
||||
Ash resource representing a custom field value for a member.
|
||||
|
||||
## Overview
|
||||
CustomFieldValues implement the Entity-Attribute-Value (EAV) pattern, allowing
|
||||
dynamic custom fields to be attached to members. Each custom field value links a
|
||||
member to a custom field and stores the actual value.
|
||||
|
||||
## Value Storage
|
||||
Values are stored using Ash's union type with JSONB storage format:
|
||||
```json
|
||||
{
|
||||
"type": "string",
|
||||
"value": "example"
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Types
|
||||
- `:string` - Text data
|
||||
- `:integer` - Numeric data
|
||||
- `:boolean` - True/false flags
|
||||
- `:date` - Date values
|
||||
- `:email` - Validated email addresses (custom type)
|
||||
|
||||
## Relationships
|
||||
- `belongs_to :member` - The member this custom field value belongs to (CASCADE delete)
|
||||
- `belongs_to :custom_field` - The custom field definition
|
||||
|
||||
## Constraints
|
||||
- Each member can have only one custom field value per custom field (unique composite index)
|
||||
- Custom field values are deleted when the associated member is deleted (CASCADE)
|
||||
- String values maximum length: 10,000 characters
|
||||
- Email values maximum length: 254 characters (RFC 5321)
|
||||
|
||||
## Future Features
|
||||
- Type-matching validation (value type must match custom field's value_type) - to be implemented
|
||||
"""
|
||||
use Ash.Resource,
|
||||
domain: Mv.Membership,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
postgres do
|
||||
table "custom_field_values"
|
||||
repo Mv.Repo
|
||||
|
||||
references do
|
||||
reference :member, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
default_accept [:value, :member_id, :custom_field_id]
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :value, :union,
|
||||
constraints: [
|
||||
storage: :type_and_value,
|
||||
types: [
|
||||
boolean: [
|
||||
type: :boolean
|
||||
],
|
||||
date: [
|
||||
type: :date
|
||||
],
|
||||
integer: [
|
||||
type: :integer
|
||||
],
|
||||
string: [
|
||||
type: :string,
|
||||
constraints: [
|
||||
max_length: 10_000,
|
||||
trim?: true
|
||||
]
|
||||
],
|
||||
email: [
|
||||
type: Mv.Membership.Email
|
||||
]
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :member, Mv.Membership.Member
|
||||
|
||||
belongs_to :custom_field, Mv.Membership.CustomField
|
||||
end
|
||||
|
||||
calculations do
|
||||
calculate :value_to_string, :string, expr(value[:value] <> "")
|
||||
end
|
||||
|
||||
# Ensure a member can only have one custom field value per custom field
|
||||
# For example: A member can have only one "phone" custom field value, one "email" custom field value, etc.
|
||||
identities do
|
||||
identity :unique_custom_field_per_member, [:member_id, :custom_field_id]
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -17,6 +17,7 @@ defmodule MvWeb.Layouts.Navbar do
|
|||
<a class="btn btn-ghost text-xl">Mitgliederverwaltung</a>
|
||||
<ul class="menu menu-horizontal bg-base-200">
|
||||
<li><.link navigate="/members">{gettext("Members")}</.link></li>
|
||||
<li><.link navigate="/custom_fields">{gettext("Custom Fields")}</.link></li>
|
||||
<li><.link navigate="/users">{gettext("Users")}</.link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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.")}
|
||||
</:subtitle>
|
||||
</.header>
|
||||
|
||||
<.form for={@form} id="property_type-form" phx-change="validate" phx-submit="save">
|
||||
<.form for={@form} id="custom_field-form" phx-change="validate" phx-submit="save">
|
||||
<.input field={@form[:name]} type="text" label={gettext("Name")} />
|
||||
<.input
|
||||
field={@form[: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>
|
||||
<.button navigate={return_path(@return_to, @property_type)}>{gettext("Cancel")}</.button>
|
||||
<.button navigate={return_path(@return_to, @custom_field)}>{gettext("Cancel")}</.button>
|
||||
</.form>
|
||||
</Layouts.app>
|
||||
"""
|
||||
|
|
@ -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
|
||||
88
lib/mv_web/live/custom_field_live/index.ex
Normal file
88
lib/mv_web/live/custom_field_live/index.ex
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
defmodule MvWeb.CustomFieldLive.Index do
|
||||
@moduledoc """
|
||||
LiveView for managing custom field definitions (admin).
|
||||
|
||||
## Features
|
||||
- List all custom fields
|
||||
- Display type information (name, value type, description)
|
||||
- Show immutable and required flags
|
||||
- Create new custom fields
|
||||
- Edit existing custom fields
|
||||
- Delete custom fields (if no custom field values use them)
|
||||
|
||||
## Displayed Information
|
||||
- Name: Unique identifier for the custom field
|
||||
- Value type: Data type constraint (string, integer, boolean, date, email)
|
||||
- Description: Human-readable explanation
|
||||
- Immutable: Whether custom field values can be changed after creation
|
||||
- Required: Whether all members must have this custom field (future feature)
|
||||
|
||||
## Events
|
||||
- `delete` - Remove a custom field (only if no custom field values exist)
|
||||
|
||||
## Security
|
||||
Custom field management is restricted to admin users.
|
||||
"""
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
Listing Custom fields
|
||||
<:actions>
|
||||
<.button variant="primary" navigate={~p"/custom_fields/new"}>
|
||||
<.icon name="hero-plus" /> New Custom field
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.table
|
||||
id="custom_fields"
|
||||
rows={@streams.custom_fields}
|
||||
row_click={fn {_id, custom_field} -> JS.navigate(~p"/custom_fields/#{custom_field}") end}
|
||||
>
|
||||
<:col :let={{_id, custom_field}} label="Id">{custom_field.id}</:col>
|
||||
|
||||
<:col :let={{_id, custom_field}} label="Name">{custom_field.name}</:col>
|
||||
|
||||
<:col :let={{_id, custom_field}} label="Description">{custom_field.description}</:col>
|
||||
|
||||
<:action :let={{_id, custom_field}}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/custom_fields/#{custom_field}"}>Show</.link>
|
||||
</div>
|
||||
|
||||
<.link navigate={~p"/custom_fields/#{custom_field}/edit"}>Edit</.link>
|
||||
</:action>
|
||||
|
||||
<:action :let={{id, custom_field}}>
|
||||
<.link
|
||||
phx-click={JS.push("delete", value: %{id: custom_field.id}) |> hide("##{id}")}
|
||||
data-confirm="Are you sure?"
|
||||
>
|
||||
Delete
|
||||
</.link>
|
||||
</:action>
|
||||
</.table>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Listing Custom fields")
|
||||
|> stream(:custom_fields, Ash.read!(Mv.Membership.CustomField))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
custom_field = Ash.get!(Mv.Membership.CustomField, id)
|
||||
Ash.destroy!(custom_field)
|
||||
|
||||
{:noreply, stream_delete(socket, :custom_fields, custom_field)}
|
||||
end
|
||||
end
|
||||
66
lib/mv_web/live/custom_field_live/show.ex
Normal file
66
lib/mv_web/live/custom_field_live/show.ex
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
defmodule MvWeb.CustomFieldLive.Show do
|
||||
@moduledoc """
|
||||
LiveView for displaying a single custom field's details (admin).
|
||||
|
||||
## Features
|
||||
- Display custom field definition
|
||||
- Show all attributes (name, value type, description, flags)
|
||||
- Navigate to edit form
|
||||
- Return to custom field list
|
||||
|
||||
## Displayed Information
|
||||
- Name: Unique identifier
|
||||
- Value type: Data type constraint
|
||||
- Description: Optional explanation
|
||||
- Immutable flag: Whether values can be changed
|
||||
- Required flag: Whether all members need this custom field
|
||||
|
||||
## Navigation
|
||||
- Back to custom field list
|
||||
- Edit custom field
|
||||
|
||||
## Security
|
||||
Custom field details are restricted to admin users.
|
||||
"""
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
Custom field {@custom_field.id}
|
||||
<:subtitle>This is a custom_field record from your database.</:subtitle>
|
||||
|
||||
<:actions>
|
||||
<.button navigate={~p"/custom_fields"}>
|
||||
<.icon name="hero-arrow-left" />
|
||||
</.button>
|
||||
<.button
|
||||
variant="primary"
|
||||
navigate={~p"/custom_fields/#{@custom_field}/edit?return_to=show"}
|
||||
>
|
||||
<.icon name="hero-pencil-square" /> Edit Custom field
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.list>
|
||||
<:item title="Id">{@custom_field.id}</:item>
|
||||
|
||||
<:item title="Name">{@custom_field.name}</:item>
|
||||
|
||||
<:item title="Description">{@custom_field.description}</:item>
|
||||
</.list>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(%{"id" => id}, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Show Custom field")
|
||||
|> assign(:custom_field, Ash.get!(Mv.Membership.CustomField, id))}
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
|||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>{gettext("Use this form to manage property records in your database.")}</:subtitle>
|
||||
<:subtitle>
|
||||
{gettext("Use this form to manage custom_field_value records in your database.")}
|
||||
</:subtitle>
|
||||
</.header>
|
||||
|
||||
<.form for={@form} id="property-form" phx-change="validate" phx-submit="save">
|
||||
<!-- Property Type Selection -->
|
||||
<.form for={@form} id="custom_field_value-form" phx-change="validate" phx-submit="save">
|
||||
<!-- Custom Field Selection -->
|
||||
<.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")}
|
||||
/>
|
||||
|
||||
<!-- Member Selection -->
|
||||
|
|
@ -61,18 +63,18 @@ defmodule MvWeb.PropertyLive.Form do
|
|||
/>
|
||||
|
||||
<!-- Value Input - handles Union type -->
|
||||
<%= 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 %>
|
||||
<div class="text-sm text-gray-600">
|
||||
{gettext("Please select a property type first")}
|
||||
{gettext("Please select a custom field first")}
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
||||
{gettext("Save Property")}
|
||||
{gettext("Save Custom field value")}
|
||||
</.button>
|
||||
<.button navigate={return_path(@return_to, @property)}>{gettext("Cancel")}</.button>
|
||||
<.button navigate={return_path(@return_to, @custom_field_value)}>{gettext("Cancel")}</.button>
|
||||
</.form>
|
||||
</Layouts.app>
|
||||
"""
|
||||
|
|
@ -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")}
|
||||
</label>
|
||||
|
||||
<%= 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
|
|||
</.inputs_for>
|
||||
<% _ -> %>
|
||||
<div class="text-sm text-red-600">
|
||||
{gettext("Unsupported value type: %{type}", type: @property_type.value_type)}
|
||||
{gettext("Unsupported value type: %{type}", type: @custom_field.value_type)}
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
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
|
||||
86
lib/mv_web/live/custom_field_value_live/index.ex
Normal file
86
lib/mv_web/live/custom_field_value_live/index.ex
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
defmodule MvWeb.CustomFieldValueLive.Index do
|
||||
@moduledoc """
|
||||
LiveView for displaying and managing custom field values.
|
||||
|
||||
## Features
|
||||
- List all custom field values with their values and types
|
||||
- Show which member each custom field value belongs to
|
||||
- Display custom field information
|
||||
- Navigate to custom field value details and edit forms
|
||||
- Delete custom field values
|
||||
|
||||
## Relationships
|
||||
Each custom field value is linked to:
|
||||
- A member (the custom field value owner)
|
||||
- A custom field (defining value type and behavior)
|
||||
|
||||
## Events
|
||||
- `delete` - Remove a custom field value from the database
|
||||
|
||||
## Note
|
||||
Custom field values are typically managed through the member edit form.
|
||||
This view provides a global overview of all custom field values.
|
||||
"""
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
Listing Custom field values
|
||||
<:actions>
|
||||
<.button variant="primary" navigate={~p"/custom_field_values/new"}>
|
||||
<.icon name="hero-plus" /> New Custom field value
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.table
|
||||
id="custom_field_values"
|
||||
rows={@streams.custom_field_values}
|
||||
row_click={
|
||||
fn {_id, custom_field_value} ->
|
||||
JS.navigate(~p"/custom_field_values/#{custom_field_value}")
|
||||
end
|
||||
}
|
||||
>
|
||||
<:col :let={{_id, custom_field_value}} label="Id">{custom_field_value.id}</:col>
|
||||
|
||||
<:action :let={{_id, custom_field_value}}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/custom_field_values/#{custom_field_value}"}>Show</.link>
|
||||
</div>
|
||||
|
||||
<.link navigate={~p"/custom_field_values/#{custom_field_value}/edit"}>Edit</.link>
|
||||
</:action>
|
||||
|
||||
<:action :let={{id, custom_field_value}}>
|
||||
<.link
|
||||
phx-click={JS.push("delete", value: %{id: custom_field_value.id}) |> hide("##{id}")}
|
||||
data-confirm="Are you sure?"
|
||||
>
|
||||
Delete
|
||||
</.link>
|
||||
</:action>
|
||||
</.table>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Listing Custom field values")
|
||||
|> stream(:custom_field_values, Ash.read!(Mv.Membership.CustomFieldValue))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
custom_field_value = Ash.get!(Mv.Membership.CustomFieldValue, id)
|
||||
Ash.destroy!(custom_field_value)
|
||||
|
||||
{:noreply, stream_delete(socket, :custom_field_values, custom_field_value)}
|
||||
end
|
||||
end
|
||||
67
lib/mv_web/live/custom_field_value_live/show.ex
Normal file
67
lib/mv_web/live/custom_field_value_live/show.ex
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
defmodule MvWeb.CustomFieldValueLive.Show do
|
||||
@moduledoc """
|
||||
LiveView for displaying a single custom field value's details.
|
||||
|
||||
## Features
|
||||
- Display custom field value and type
|
||||
- Show linked member
|
||||
- Show custom field definition
|
||||
- Navigate to edit form
|
||||
- Return to custom field value list
|
||||
|
||||
## Displayed Information
|
||||
- Custom field value (formatted based on type)
|
||||
- Custom field name and description
|
||||
- Member information (who owns this custom field value)
|
||||
- Custom field value metadata (ID, timestamps if added)
|
||||
|
||||
## Navigation
|
||||
- Back to custom field value list
|
||||
- Edit custom field value
|
||||
"""
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
Custom field value {@custom_field_value.id}
|
||||
<:subtitle>This is a custom_field_value record from your database.</:subtitle>
|
||||
|
||||
<:actions>
|
||||
<.button navigate={~p"/custom_field_values"}>
|
||||
<.icon name="hero-arrow-left" />
|
||||
</.button>
|
||||
<.button
|
||||
variant="primary"
|
||||
navigate={~p"/custom_field_values/#{@custom_field_value}/edit?return_to=show"}
|
||||
>
|
||||
<.icon name="hero-pencil-square" /> Edit Custom field value
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.list>
|
||||
<:item title="Id">{@custom_field_value.id}</:item>
|
||||
</.list>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"id" => id}, _, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_title, page_title(socket.assigns.live_action))
|
||||
|> assign(:custom_field_value, Ash.get!(Mv.Membership.CustomFieldValue, id))}
|
||||
end
|
||||
|
||||
defp page_title(:show), do: "Show Custom field value"
|
||||
defp page_title(:edit), do: "Edit Custom field value"
|
||||
end
|
||||
|
|
@ -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")} />
|
||||
|
||||
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Properties")}</h3>
|
||||
<.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]}>
|
||||
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Field Values")}</h3>
|
||||
<.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
|
|||
</.inputs_for>
|
||||
<input
|
||||
type="hidden"
|
||||
name={f_property[:property_type_id].name}
|
||||
value={f_property[:property_type_id].value}
|
||||
name={f_custom_field_value[:custom_field_id].name}
|
||||
value={f_custom_field_value[:custom_field_id].value}
|
||||
/>
|
||||
</.inputs_for>
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|||
</:item>
|
||||
</.list>
|
||||
|
||||
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Properties")}</h3>
|
||||
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Field Values")}</h3>
|
||||
<.generic_list items={
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
defmodule MvWeb.PropertyLive.Index do
|
||||
@moduledoc """
|
||||
LiveView for displaying and managing properties.
|
||||
|
||||
## Features
|
||||
- List all properties with their values and types
|
||||
- Show which member each property belongs to
|
||||
- Display property type information
|
||||
- Navigate to property details and edit forms
|
||||
- Delete properties
|
||||
|
||||
## Relationships
|
||||
Each property is linked to:
|
||||
- A member (the property owner)
|
||||
- A property type (defining value type and behavior)
|
||||
|
||||
## Events
|
||||
- `delete` - Remove a property from the database
|
||||
|
||||
## Note
|
||||
Properties are typically managed through the member edit form.
|
||||
This view provides a global overview of all properties.
|
||||
"""
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
Listing Properties
|
||||
<:actions>
|
||||
<.button variant="primary" navigate={~p"/properties/new"}>
|
||||
<.icon name="hero-plus" /> New Property
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.table
|
||||
id="properties"
|
||||
rows={@streams.properties}
|
||||
row_click={fn {_id, property} -> JS.navigate(~p"/properties/#{property}") end}
|
||||
>
|
||||
<:col :let={{_id, property}} label="Id">{property.id}</:col>
|
||||
|
||||
<:action :let={{_id, property}}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/properties/#{property}"}>Show</.link>
|
||||
</div>
|
||||
|
||||
<.link navigate={~p"/properties/#{property}/edit"}>Edit</.link>
|
||||
</:action>
|
||||
|
||||
<:action :let={{id, property}}>
|
||||
<.link
|
||||
phx-click={JS.push("delete", value: %{id: property.id}) |> hide("##{id}")}
|
||||
data-confirm="Are you sure?"
|
||||
>
|
||||
Delete
|
||||
</.link>
|
||||
</:action>
|
||||
</.table>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Listing Properties")
|
||||
|> stream(:properties, Ash.read!(Mv.Membership.Property))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
property = Ash.get!(Mv.Membership.Property, id)
|
||||
Ash.destroy!(property)
|
||||
|
||||
{:noreply, stream_delete(socket, :properties, property)}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
defmodule MvWeb.PropertyLive.Show do
|
||||
@moduledoc """
|
||||
LiveView for displaying a single property's details.
|
||||
|
||||
## Features
|
||||
- Display property value and type
|
||||
- Show linked member
|
||||
- Show property type definition
|
||||
- Navigate to edit form
|
||||
- Return to property list
|
||||
|
||||
## Displayed Information
|
||||
- Property value (formatted based on type)
|
||||
- Property type name and description
|
||||
- Member information (who owns this property)
|
||||
- Property metadata (ID, timestamps if added)
|
||||
|
||||
## Navigation
|
||||
- Back to property list
|
||||
- Edit property
|
||||
"""
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
Property {@property.id}
|
||||
<:subtitle>This is a property record from your database.</:subtitle>
|
||||
|
||||
<:actions>
|
||||
<.button navigate={~p"/properties"}>
|
||||
<.icon name="hero-arrow-left" />
|
||||
</.button>
|
||||
<.button variant="primary" navigate={~p"/properties/#{@property}/edit?return_to=show"}>
|
||||
<.icon name="hero-pencil-square" /> Edit Property
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.list>
|
||||
<:item title="Id">{@property.id}</:item>
|
||||
</.list>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"id" => id}, _, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_title, page_title(socket.assigns.live_action))
|
||||
|> assign(:property, Ash.get!(Mv.Membership.Property, id))}
|
||||
end
|
||||
|
||||
defp page_title(:show), do: "Show Property"
|
||||
defp page_title(:edit), do: "Edit Property"
|
||||
end
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
defmodule MvWeb.PropertyTypeLive.Index do
|
||||
@moduledoc """
|
||||
LiveView for managing property type definitions (admin).
|
||||
|
||||
## Features
|
||||
- List all property types
|
||||
- Display type information (name, value type, description)
|
||||
- Show immutable and required flags
|
||||
- Create new property types
|
||||
- Edit existing property types
|
||||
- Delete property types (if no properties use them)
|
||||
|
||||
## Displayed Information
|
||||
- Name: Unique identifier for the property type
|
||||
- Value type: Data type constraint (string, integer, boolean, date, email)
|
||||
- Description: Human-readable explanation
|
||||
- Immutable: Whether property values can be changed after creation
|
||||
- Required: Whether all members must have this property (future feature)
|
||||
|
||||
## Events
|
||||
- `delete` - Remove a property type (only if no properties exist)
|
||||
|
||||
## Security
|
||||
Property type management is restricted to admin users.
|
||||
"""
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
Listing Property types
|
||||
<:actions>
|
||||
<.button variant="primary" navigate={~p"/property_types/new"}>
|
||||
<.icon name="hero-plus" /> New Property type
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.table
|
||||
id="property_types"
|
||||
rows={@streams.property_types}
|
||||
row_click={fn {_id, property_type} -> JS.navigate(~p"/property_types/#{property_type}") end}
|
||||
>
|
||||
<:col :let={{_id, property_type}} label="Id">{property_type.id}</:col>
|
||||
|
||||
<:col :let={{_id, property_type}} label="Name">{property_type.name}</:col>
|
||||
|
||||
<:col :let={{_id, property_type}} label="Description">{property_type.description}</:col>
|
||||
|
||||
<:action :let={{_id, property_type}}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/property_types/#{property_type}"}>Show</.link>
|
||||
</div>
|
||||
|
||||
<.link navigate={~p"/property_types/#{property_type}/edit"}>Edit</.link>
|
||||
</:action>
|
||||
|
||||
<:action :let={{id, property_type}}>
|
||||
<.link
|
||||
phx-click={JS.push("delete", value: %{id: property_type.id}) |> hide("##{id}")}
|
||||
data-confirm="Are you sure?"
|
||||
>
|
||||
Delete
|
||||
</.link>
|
||||
</:action>
|
||||
</.table>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Listing Property types")
|
||||
|> stream(:property_types, Ash.read!(Mv.Membership.PropertyType))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
property_type = Ash.get!(Mv.Membership.PropertyType, id)
|
||||
Ash.destroy!(property_type)
|
||||
|
||||
{:noreply, stream_delete(socket, :property_types, property_type)}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
defmodule MvWeb.PropertyTypeLive.Show do
|
||||
@moduledoc """
|
||||
LiveView for displaying a single property type's details (admin).
|
||||
|
||||
## Features
|
||||
- Display property type definition
|
||||
- Show all attributes (name, value type, description, flags)
|
||||
- Navigate to edit form
|
||||
- Return to property type list
|
||||
|
||||
## Displayed Information
|
||||
- Name: Unique identifier
|
||||
- Value type: Data type constraint
|
||||
- Description: Optional explanation
|
||||
- Immutable flag: Whether values can be changed
|
||||
- Required flag: Whether all members need this property
|
||||
|
||||
## Navigation
|
||||
- Back to property type list
|
||||
- Edit property type
|
||||
|
||||
## Security
|
||||
Property type details are restricted to admin users.
|
||||
"""
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.header>
|
||||
Property type {@property_type.id}
|
||||
<:subtitle>This is a property_type record from your database.</:subtitle>
|
||||
|
||||
<:actions>
|
||||
<.button navigate={~p"/property_types"}>
|
||||
<.icon name="hero-arrow-left" />
|
||||
</.button>
|
||||
<.button
|
||||
variant="primary"
|
||||
navigate={~p"/property_types/#{@property_type}/edit?return_to=show"}
|
||||
>
|
||||
<.icon name="hero-pencil-square" /> Edit Property type
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.list>
|
||||
<:item title="Id">{@property_type.id}</:item>
|
||||
|
||||
<:item title="Name">{@property_type.name}</:item>
|
||||
|
||||
<:item title="Description">{@property_type.description}</:item>
|
||||
</.list>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(%{"id" => id}, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Show Property type")
|
||||
|> assign(:property_type, Ash.get!(Mv.Membership.PropertyType, id))}
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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!")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
106
priv/resource_snapshots/repo/custom_fields/20251113163602.json
Normal file
106
priv/resource_snapshots/repo/custom_fields/20251113163602.json
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "value_type",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "description",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "immutable",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "required",
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "B98535258034AE3C37FCB7AF054B97D7CCADE3CA7015B1B93C64CDE1250807EE",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "custom_fields_unique_name_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "name"
|
||||
}
|
||||
],
|
||||
"name": "unique_name",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.Mv.Repo",
|
||||
"schema": null,
|
||||
"table": "custom_fields"
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
"ash_functions_version": 5,
|
||||
"installed": [
|
||||
"ash-functions",
|
||||
"citext"
|
||||
"citext",
|
||||
"pg_trgm"
|
||||
]
|
||||
}
|
||||
205
test/membership/custom_field_validation_test.exs
Normal file
205
test/membership/custom_field_validation_test.exs
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
defmodule Mv.Membership.CustomFieldValidationTest do
|
||||
@moduledoc """
|
||||
Tests for CustomField validation constraints.
|
||||
|
||||
Tests cover:
|
||||
- Name length validation (max 100 characters)
|
||||
- Name trimming
|
||||
- Description length validation (max 500 characters)
|
||||
- Description trimming
|
||||
- Required vs optional fields
|
||||
"""
|
||||
use Mv.DataCase, async: true
|
||||
|
||||
alias Mv.Membership.CustomField
|
||||
|
||||
describe "name validation" do
|
||||
test "accepts name with exactly 100 characters" do
|
||||
name = String.duplicate("a", 100)
|
||||
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: name,
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field.name == name
|
||||
assert String.length(custom_field.name) == 100
|
||||
end
|
||||
|
||||
test "rejects name with 101 characters" do
|
||||
name = String.duplicate("a", 101)
|
||||
|
||||
assert {:error, changeset} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: name,
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert [%{field: :name, message: message}] = changeset.errors
|
||||
assert message =~ "max" or message =~ "length" or message =~ "100"
|
||||
end
|
||||
|
||||
test "trims whitespace from name" do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: " test_field ",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field.name == "test_field"
|
||||
end
|
||||
|
||||
test "rejects empty name" do
|
||||
assert {:error, changeset} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error -> error.field == :name end)
|
||||
end
|
||||
|
||||
test "rejects nil name" do
|
||||
assert {:error, changeset} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error -> error.field == :name end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "description validation" do
|
||||
test "accepts description with exactly 500 characters" do
|
||||
description = String.duplicate("a", 500)
|
||||
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field",
|
||||
value_type: :string,
|
||||
description: description
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field.description == description
|
||||
assert String.length(custom_field.description) == 500
|
||||
end
|
||||
|
||||
test "rejects description with 501 characters" do
|
||||
description = String.duplicate("a", 501)
|
||||
|
||||
assert {:error, changeset} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field",
|
||||
value_type: :string,
|
||||
description: description
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert [%{field: :description, message: message}] = changeset.errors
|
||||
assert message =~ "max" or message =~ "length" or message =~ "500"
|
||||
end
|
||||
|
||||
test "trims whitespace from description" do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field",
|
||||
value_type: :string,
|
||||
description: " A nice description "
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field.description == "A nice description"
|
||||
end
|
||||
|
||||
test "accepts nil description (optional field)" do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field.description == nil
|
||||
end
|
||||
|
||||
test "accepts empty description after trimming" do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field",
|
||||
value_type: :string,
|
||||
description: " "
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# After trimming whitespace, becomes nil (empty strings are converted to nil)
|
||||
assert custom_field.description == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "name uniqueness" do
|
||||
test "rejects duplicate names" do
|
||||
assert {:ok, _} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "unique_field",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert {:error, changeset} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "unique_field",
|
||||
value_type: :integer
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error -> error.field == :name end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "value_type validation" do
|
||||
test "accepts all valid value types" do
|
||||
for value_type <- [:string, :integer, :boolean, :date, :email] do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "field_#{value_type}",
|
||||
value_type: value_type
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field.value_type == value_type
|
||||
end
|
||||
end
|
||||
|
||||
test "rejects invalid value type" do
|
||||
assert {:error, changeset} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "invalid_field",
|
||||
value_type: :invalid_type
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert [%{field: :value_type}] = changeset.errors
|
||||
end
|
||||
end
|
||||
end
|
||||
305
test/membership/custom_field_value_validation_test.exs
Normal file
305
test/membership/custom_field_value_validation_test.exs
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
defmodule Mv.Membership.CustomFieldValueValidationTest do
|
||||
@moduledoc """
|
||||
Tests for CustomFieldValue validation constraints.
|
||||
|
||||
Tests cover:
|
||||
- String value length validation (max 10,000 characters)
|
||||
- String value trimming
|
||||
- Email value validation (via Email type)
|
||||
- Optional values (nil allowed)
|
||||
"""
|
||||
use Mv.DataCase, async: true
|
||||
|
||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
# Create a test member
|
||||
{:ok, member} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
email: "test.validation@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create custom fields for different types
|
||||
{:ok, string_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "string_field",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, integer_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "integer_field",
|
||||
value_type: :integer
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, email_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "email_field",
|
||||
value_type: :email
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
%{
|
||||
member: member,
|
||||
string_field: string_field,
|
||||
integer_field: integer_field,
|
||||
email_field: email_field
|
||||
}
|
||||
end
|
||||
|
||||
describe "string value length validation" do
|
||||
test "accepts string value with exactly 10,000 characters", %{
|
||||
member: member,
|
||||
string_field: string_field
|
||||
} do
|
||||
value_string = String.duplicate("a", 10_000)
|
||||
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: string_field.id,
|
||||
value: %{
|
||||
"_union_type" => "string",
|
||||
"_union_value" => value_string
|
||||
}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field_value.value.value == value_string
|
||||
assert String.length(custom_field_value.value.value) == 10_000
|
||||
end
|
||||
|
||||
test "rejects string value with 10,001 characters", %{
|
||||
member: member,
|
||||
string_field: string_field
|
||||
} do
|
||||
value_string = String.duplicate("a", 10_001)
|
||||
|
||||
assert {:error, changeset} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => value_string}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error ->
|
||||
error.field == :value and (error.message =~ "max" or error.message =~ "length")
|
||||
end)
|
||||
end
|
||||
|
||||
test "trims whitespace from string value", %{member: member, string_field: string_field} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => " test value "}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field_value.value.value == "test value"
|
||||
end
|
||||
|
||||
test "accepts empty string value", %{member: member, string_field: string_field} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => ""}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Empty strings after trimming become nil
|
||||
assert custom_field_value.value.value == nil
|
||||
end
|
||||
|
||||
test "accepts string with special characters", %{member: member, string_field: string_field} do
|
||||
special_string = "Hello 世界! 🎉 @#$%^&*()"
|
||||
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => special_string}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field_value.value.value == special_string
|
||||
end
|
||||
end
|
||||
|
||||
describe "integer value validation" do
|
||||
test "accepts valid integer value", %{member: member, integer_field: integer_field} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: integer_field.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 42}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field_value.value.value == 42
|
||||
end
|
||||
|
||||
test "accepts negative integer", %{member: member, integer_field: integer_field} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: integer_field.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => -100}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field_value.value.value == -100
|
||||
end
|
||||
|
||||
test "accepts zero", %{member: member, integer_field: integer_field} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: integer_field.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 0}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field_value.value.value == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "email value validation" do
|
||||
test "accepts nil value (optional field)", %{member: member, email_field: email_field} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => nil}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field_value.value.value == nil
|
||||
end
|
||||
|
||||
test "accepts empty string (becomes nil after trim)", %{
|
||||
member: member,
|
||||
email_field: email_field
|
||||
} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => ""}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Empty string after trim should become nil
|
||||
assert custom_field_value.value.value == nil
|
||||
end
|
||||
|
||||
test "accepts valid email", %{member: member, email_field: email_field} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => "test@example.com"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field_value.value.value == "test@example.com"
|
||||
end
|
||||
|
||||
test "rejects invalid email format", %{member: member, email_field: email_field} do
|
||||
assert {:error, changeset} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => "not-an-email"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error -> error.field == :value end)
|
||||
end
|
||||
|
||||
test "rejects email longer than 254 characters", %{member: member, email_field: email_field} do
|
||||
# Create an email with >254 chars (243 + 12 = 255)
|
||||
long_email = String.duplicate("a", 243) <> "@example.com"
|
||||
|
||||
assert {:error, changeset} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => long_email}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert Enum.any?(changeset.errors, fn error -> error.field == :value end)
|
||||
end
|
||||
|
||||
test "trims whitespace from email", %{member: member, email_field: email_field} do
|
||||
assert {:ok, custom_field_value} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: email_field.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => " test@example.com "}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field_value.value.value == "test@example.com"
|
||||
end
|
||||
end
|
||||
|
||||
describe "uniqueness constraint" do
|
||||
test "rejects duplicate custom_field_id per member", %{
|
||||
member: member,
|
||||
string_field: string_field
|
||||
} do
|
||||
# Create first custom field value
|
||||
assert {:ok, _} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "first value"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Try to create second custom field value with same custom_field_id for same member
|
||||
assert {:error, changeset} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: string_field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "second value"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Should have uniqueness error
|
||||
assert Enum.any?(changeset.errors, fn error ->
|
||||
error.message =~ "unique" or error.message =~ "already exists" or
|
||||
error.message =~ "has already been taken"
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -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"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue