Add boolean custom field filters to member overview closes #309 #362
41 changed files with 1886 additions and 6203 deletions
35
CHANGELOG.md
35
CHANGELOG.md
|
|
@ -8,6 +8,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- **Roles and Permissions System (RBAC)** - Complete implementation (#345, 2026-01-08)
|
||||||
|
- Four hardcoded permission sets: `own_data`, `read_only`, `normal_user`, `admin`
|
||||||
|
- Database-backed roles with permission set references
|
||||||
|
- Member resource policies with scope filtering (`:own`, `:linked`, `:all`)
|
||||||
|
- Authorization checks via `Mv.Authorization.Checks.HasPermission`
|
||||||
|
- System role protection (critical roles cannot be deleted)
|
||||||
|
- Role management UI at `/admin/roles`
|
||||||
|
- **Membership Fees System** - Full implementation
|
||||||
|
- Membership fee types with intervals (monthly, quarterly, half_yearly, yearly)
|
||||||
|
- Individual billing cycles per member with payment status tracking
|
||||||
|
- Cycle generation and regeneration
|
||||||
|
- Global membership fee settings
|
||||||
|
- UI components for fee management
|
||||||
|
- **Global Settings Management** - Singleton settings resource
|
||||||
|
- Club name configuration (with environment variable support)
|
||||||
|
- Member field visibility settings
|
||||||
|
- Membership fee default settings
|
||||||
|
- **Sidebar Navigation** - Replaced navbar with standard-compliant sidebar (#260, 2026-01-12)
|
||||||
|
- **CSV Import Templates** - German and English templates (#329, 2026-01-13)
|
||||||
|
- Template files in `priv/static/templates/`
|
||||||
|
- CSV specification documented
|
||||||
- User-Member linking with fuzzy search autocomplete (#168)
|
- User-Member linking with fuzzy search autocomplete (#168)
|
||||||
- PostgreSQL trigram-based member search with typo tolerance
|
- PostgreSQL trigram-based member search with typo tolerance
|
||||||
- WCAG 2.1 AA compliant autocomplete dropdown with ARIA support
|
- WCAG 2.1 AA compliant autocomplete dropdown with ARIA support
|
||||||
|
|
@ -19,8 +40,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- German/English translations
|
- German/English translations
|
||||||
- Docker secrets support via `_FILE` environment variables for all sensitive configuration (SECRET_KEY_BASE, TOKEN_SIGNING_SECRET, OIDC_CLIENT_SECRET, DATABASE_URL, DATABASE_PASSWORD)
|
- Docker secrets support via `_FILE` environment variables for all sensitive configuration (SECRET_KEY_BASE, TOKEN_SIGNING_SECRET, OIDC_CLIENT_SECRET, DATABASE_URL, DATABASE_PASSWORD)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Actor Handling Refactoring** (2026-01-09)
|
||||||
|
- Standardized actor access with `current_actor/1` helper function
|
||||||
|
- `ash_actor_opts/1` helper for consistent authorization options
|
||||||
|
- `submit_form/3` wrapper for form submissions with actor
|
||||||
|
- All Ash operations now properly pass `actor` parameter
|
||||||
|
- **Error Handling Improvements** (2026-01-13)
|
||||||
|
- Replaced `Ash.read!` with proper error handling in LiveViews
|
||||||
|
- Consistent flash message handling for authorization errors
|
||||||
|
- Early return patterns for unauthenticated users
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Email validation false positive when linking user and member with identical emails (#168 Problem #4)
|
- Email validation false positive when linking user and member with identical emails (#168 Problem #4)
|
||||||
- Relationship data extraction from Ash manage_relationship during validation
|
- Relationship data extraction from Ash manage_relationship during validation
|
||||||
- Copy button count now shows only visible selected members when filtering
|
- Copy button count now shows only visible selected members when filtering
|
||||||
|
- Language headers in German `.po` files (corrected from "en" to "de")
|
||||||
|
- Critical deny-filter bug in authorization system (2026-01-08)
|
||||||
|
- HasPermission auto_filter and strict_check implementation (2026-01-08)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,18 @@ lib/
|
||||||
│ ├── member.ex # Member resource
|
│ ├── member.ex # Member resource
|
||||||
│ ├── custom_field_value.ex # Custom field value resource
|
│ ├── custom_field_value.ex # Custom field value resource
|
||||||
│ ├── custom_field.ex # CustomFieldValue type resource
|
│ ├── custom_field.ex # CustomFieldValue type resource
|
||||||
|
│ ├── setting.ex # Global settings (singleton resource)
|
||||||
│ └── email.ex # Email custom type
|
│ └── email.ex # Email custom type
|
||||||
|
├── membership_fees/ # MembershipFees domain
|
||||||
|
│ ├── membership_fees.ex # Domain definition
|
||||||
|
│ ├── membership_fee_type.ex # Membership fee type resource
|
||||||
|
│ ├── membership_fee_cycle.ex # Membership fee cycle resource
|
||||||
|
│ └── changes/ # Ash changes for membership fees
|
||||||
|
├── mv/authorization/ # Authorization domain
|
||||||
|
│ ├── authorization.ex # Domain definition
|
||||||
|
│ ├── role.ex # Role resource
|
||||||
|
│ ├── permission_sets.ex # Hardcoded permission sets
|
||||||
|
│ └── checks/ # Authorization checks
|
||||||
├── mv/ # Core application modules
|
├── mv/ # Core application modules
|
||||||
│ ├── accounts/ # Domain-specific logic
|
│ ├── accounts/ # Domain-specific logic
|
||||||
│ │ └── user/
|
│ │ └── user/
|
||||||
|
|
@ -96,6 +107,11 @@ lib/
|
||||||
│ ├── membership/ # Domain-specific logic
|
│ ├── membership/ # Domain-specific logic
|
||||||
│ │ └── member/
|
│ │ └── member/
|
||||||
│ │ └── validations/
|
│ │ └── validations/
|
||||||
|
│ ├── membership_fees/ # Membership fee business logic
|
||||||
|
│ │ ├── cycle_generator.ex # Cycle generation algorithm
|
||||||
|
│ │ └── calendar_cycles.ex # Calendar cycle calculations
|
||||||
|
│ ├── helpers.ex # Shared helper functions (ash_actor_opts)
|
||||||
|
│ ├── constants.ex # Application constants (member_fields, custom_field_prefix)
|
||||||
│ ├── application.ex # OTP application
|
│ ├── application.ex # OTP application
|
||||||
│ ├── mailer.ex # Email mailer
|
│ ├── mailer.ex # Email mailer
|
||||||
│ ├── release.ex # Release tasks
|
│ ├── release.ex # Release tasks
|
||||||
|
|
@ -107,7 +123,7 @@ lib/
|
||||||
│ │ ├── table_components.ex
|
│ │ ├── table_components.ex
|
||||||
│ │ ├── layouts.ex
|
│ │ ├── layouts.ex
|
||||||
│ │ └── layouts/ # Layout templates
|
│ │ └── layouts/ # Layout templates
|
||||||
│ │ ├── navbar.ex
|
│ │ ├── sidebar.ex
|
||||||
│ │ └── root.html.heex
|
│ │ └── root.html.heex
|
||||||
│ ├── controllers/ # HTTP controllers
|
│ ├── controllers/ # HTTP controllers
|
||||||
│ │ ├── auth_controller.ex
|
│ │ ├── auth_controller.ex
|
||||||
|
|
@ -116,6 +132,11 @@ lib/
|
||||||
│ │ ├── error_html.ex
|
│ │ ├── error_html.ex
|
||||||
│ │ ├── error_json.ex
|
│ │ ├── error_json.ex
|
||||||
│ │ └── page_html/
|
│ │ └── page_html/
|
||||||
|
│ ├── helpers/ # Web layer helper modules
|
||||||
|
│ │ ├── member_helpers.ex # Member display utilities
|
||||||
|
│ │ ├── membership_fee_helpers.ex # Membership fee formatting
|
||||||
|
│ │ ├── date_formatter.ex # Date formatting utilities
|
||||||
|
│ │ └── field_type_formatter.ex # Field type display formatting
|
||||||
│ ├── live/ # LiveView modules
|
│ ├── live/ # LiveView modules
|
||||||
│ │ ├── components/ # LiveView-specific components
|
│ │ ├── components/ # LiveView-specific components
|
||||||
│ │ │ ├── search_bar_component.ex
|
│ │ │ ├── search_bar_component.ex
|
||||||
|
|
@ -123,11 +144,16 @@ lib/
|
||||||
│ │ ├── member_live/ # Member CRUD LiveViews
|
│ │ ├── member_live/ # Member CRUD LiveViews
|
||||||
│ │ ├── custom_field_value_live/ # CustomFieldValue CRUD LiveViews
|
│ │ ├── custom_field_value_live/ # CustomFieldValue CRUD LiveViews
|
||||||
│ │ ├── custom_field_live/
|
│ │ ├── custom_field_live/
|
||||||
│ │ └── user_live/ # User management LiveViews
|
│ │ ├── user_live/ # User management LiveViews
|
||||||
|
│ │ ├── role_live/ # Role management LiveViews
|
||||||
|
│ │ ├── membership_fee_type_live/ # Membership fee type LiveViews
|
||||||
|
│ │ ├── membership_fee_settings_live.ex # Membership fee settings
|
||||||
|
│ │ ├── global_settings_live.ex # Global settings
|
||||||
|
│ │ └── contribution_type_live/ # Contribution types (mock-up)
|
||||||
│ ├── auth_overrides.ex # AshAuthentication overrides
|
│ ├── auth_overrides.ex # AshAuthentication overrides
|
||||||
│ ├── endpoint.ex # Phoenix endpoint
|
│ ├── endpoint.ex # Phoenix endpoint
|
||||||
│ ├── gettext.ex # I18n configuration
|
│ ├── gettext.ex # I18n configuration
|
||||||
│ ├── live_helpers.ex # LiveView helpers
|
│ ├── live_helpers.ex # LiveView lifecycle hooks and helpers
|
||||||
│ ├── live_user_auth.ex # LiveView authentication
|
│ ├── live_user_auth.ex # LiveView authentication
|
||||||
│ ├── router.ex # Application router
|
│ ├── router.ex # Application router
|
||||||
│ └── telemetry.ex # Telemetry configuration
|
│ └── telemetry.ex # Telemetry configuration
|
||||||
|
|
@ -176,7 +202,7 @@ test/
|
||||||
**Module Naming:**
|
**Module Naming:**
|
||||||
|
|
||||||
- **Modules:** Use `PascalCase` with full namespace (e.g., `Mv.Accounts.User`)
|
- **Modules:** Use `PascalCase` with full namespace (e.g., `Mv.Accounts.User`)
|
||||||
- **Domains:** Top-level domains are `Mv.Accounts` and `Mv.Membership`
|
- **Domains:** Top-level domains are `Mv.Accounts`, `Mv.Membership`, `Mv.MembershipFees`, and `Mv.Authorization`
|
||||||
- **Resources:** Resource modules should be singular nouns (e.g., `Member`, not `Members`)
|
- **Resources:** Resource modules should be singular nouns (e.g., `Member`, not `Members`)
|
||||||
- **Context functions:** Use `snake_case` and verb-first naming (e.g., `create_user`, `list_members`)
|
- **Context functions:** Use `snake_case` and verb-first naming (e.g., `create_user`, `list_members`)
|
||||||
|
|
||||||
|
|
@ -818,14 +844,17 @@ end
|
||||||
|
|
||||||
```heex
|
```heex
|
||||||
<!-- Leverage DaisyUI component classes -->
|
<!-- Leverage DaisyUI component classes -->
|
||||||
<div class="navbar bg-base-100">
|
<!-- Note: Navbar has been replaced with Sidebar (see lib/mv_web/components/layouts/sidebar.ex) -->
|
||||||
<div class="navbar-start">
|
<div class="drawer lg:drawer-open">
|
||||||
<a class="btn btn-ghost text-xl">Mila</a>
|
<input id="drawer-toggle" type="checkbox" class="drawer-toggle" />
|
||||||
|
<div class="drawer-content">
|
||||||
|
<!-- Page content -->
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-end">
|
<div class="drawer-side">
|
||||||
<.link navigate={~p"/members"} class="btn btn-primary">
|
<label for="drawer-toggle" class="drawer-overlay"></label>
|
||||||
Members
|
<aside class="w-64 min-h-full bg-base-200">
|
||||||
</.link>
|
<!-- Sidebar content -->
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
@ -1535,15 +1564,57 @@ policies do
|
||||||
authorize_if always()
|
authorize_if always()
|
||||||
end
|
end
|
||||||
|
|
||||||
# Specific permissions
|
# Use HasPermission check for role-based authorization
|
||||||
policy action_type([:read, :update]) do
|
policy action_type([:read, :update, :create, :destroy]) do
|
||||||
authorize_if relates_to_actor_via(:user)
|
authorize_if Mv.Authorization.Checks.HasPermission
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
policy action_type(:destroy) do
|
**Actor Handling in LiveViews:**
|
||||||
authorize_if actor_attribute_equals(:role, :admin)
|
|
||||||
|
Always use the `current_actor/1` helper for consistent actor access:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# In LiveView modules
|
||||||
|
import MvWeb.LiveHelpers, only: [current_actor: 1, ash_actor_opts: 1, submit_form: 3]
|
||||||
|
|
||||||
|
def mount(_params, _session, socket) do
|
||||||
|
actor = current_actor(socket)
|
||||||
|
|
||||||
|
case Ash.read(Mv.Membership.Member, ash_actor_opts(actor)) do
|
||||||
|
{:ok, members} ->
|
||||||
|
{:ok, assign(socket, :members, members)}
|
||||||
|
{:error, error} ->
|
||||||
|
{:ok, put_flash(socket, :error, "Failed to load members")}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("save", %{"member" => params}, socket) do
|
||||||
|
actor = current_actor(socket)
|
||||||
|
form = AshPhoenix.Form.for_create(Mv.Membership.Member, :create)
|
||||||
|
|
||||||
|
case submit_form(form, params, actor) do
|
||||||
|
{:ok, member} ->
|
||||||
|
{:noreply, push_navigate(socket, to: ~p"/members/#{member.id}")}
|
||||||
|
{:error, form} ->
|
||||||
|
{:noreply, assign(socket, :form, form)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Never use bang calls (`Ash.read!`, `Ash.get!`) without error handling:**
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Bad - will crash on authorization errors
|
||||||
|
members = Ash.read!(Mv.Membership.Member, actor: actor)
|
||||||
|
|
||||||
|
# Good - proper error handling
|
||||||
|
case Ash.read(Mv.Membership.Member, actor: actor) do
|
||||||
|
{:ok, members} -> # success
|
||||||
|
{:error, %Ash.Error.Forbidden{}} -> # handle authorization error
|
||||||
|
{:error, error} -> # handle other errors
|
||||||
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.2 Password Security
|
### 5.2 Password Security
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -40,14 +40,16 @@ Our philosophy: **software should help people spend less time on administration
|
||||||
## 🔑 Features
|
## 🔑 Features
|
||||||
|
|
||||||
- ✅ Manage member data with ease
|
- ✅ Manage member data with ease
|
||||||
- 🚧 Overview of membership fees & payment status
|
- ✅ Membership fees & payment status tracking
|
||||||
- ✅ Full-text search
|
- ✅ Full-text search with fuzzy matching
|
||||||
- 🚧 Sorting & filtering
|
- ✅ Sorting & filtering
|
||||||
- 🚧 Roles & permissions (e.g. board, treasurer)
|
- ✅ Roles & permissions (RBAC system with 4 permission sets)
|
||||||
- ✅ Custom fields (flexible per club needs)
|
- ✅ Custom fields (flexible per club needs)
|
||||||
- ✅ SSO via OIDC (works with Authentik, Rauthy, Keycloak, etc.)
|
- ✅ SSO via OIDC (works with Authentik, Rauthy, Keycloak, etc.)
|
||||||
|
- ✅ Sidebar navigation (standard-compliant, accessible)
|
||||||
|
- ✅ Global settings management
|
||||||
- 🚧 Self-service & online application
|
- 🚧 Self-service & online application
|
||||||
- 🚧 Accessibility, GDPR, usability improvements
|
- ✅ Accessibility improvements (WCAG 2.1 AA compliant keyboard navigation)
|
||||||
- 🚧 Email sending
|
- 🚧 Email sending
|
||||||
|
|
||||||
## 🚀 Quick Start (Development)
|
## 🚀 Quick Start (Development)
|
||||||
|
|
@ -187,8 +189,9 @@ The `OIDC_REDIRECT_URI` is auto-generated as `https://{DOMAIN}/auth/user/rauthy/
|
||||||
- **Auth:** AshAuthentication (OIDC + password)
|
- **Auth:** AshAuthentication (OIDC + password)
|
||||||
|
|
||||||
**Code Structure:**
|
**Code Structure:**
|
||||||
- `lib/accounts/` & `lib/membership/` — Ash resources and domains
|
- `lib/accounts/` & `lib/membership/` & `lib/membership_fees/` & `lib/mv/authorization/` — Ash resources and domains
|
||||||
- `lib/mv_web/` — Phoenix controllers, LiveViews, components
|
- `lib/mv_web/` — Phoenix controllers, LiveViews, components
|
||||||
|
- `lib/mv/` — Shared helpers and business logic
|
||||||
- `assets/` — Tailwind, JavaScript, static files
|
- `assets/` — Tailwind, JavaScript, static files
|
||||||
|
|
||||||
📚 **Full tech stack details:** See [`CODE_GUIDELINES.md`](CODE_GUIDELINES.md)
|
📚 **Full tech stack details:** See [`CODE_GUIDELINES.md`](CODE_GUIDELINES.md)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# CSV Member Import v1 - Implementation Plan
|
# CSV Member Import v1 - Implementation Plan
|
||||||
|
|
||||||
**Version:** 1.0
|
**Version:** 1.0
|
||||||
**Date:** 2025-01-XX
|
**Last Updated:** 2026-01-13
|
||||||
**Status:** In Progress (Backend Complete, UI Pending)
|
**Status:** In Progress (Backend Complete, UI Pending)
|
||||||
**Related Documents:**
|
**Related Documents:**
|
||||||
- [Feature Roadmap](./feature-roadmap.md) - Overall feature planning
|
- [Feature Roadmap](./feature-roadmap.md) - Overall feature planning
|
||||||
|
|
@ -733,4 +733,4 @@ end
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**End of Implementation Plan**
|
**End of Implementation Plan**
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,10 @@ This document provides a comprehensive overview of the Mila Membership Managemen
|
||||||
|
|
||||||
| Metric | Count |
|
| Metric | Count |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| **Tables** | 5 |
|
| **Tables** | 9 |
|
||||||
| **Domains** | 2 (Accounts, Membership) |
|
| **Domains** | 4 (Accounts, Membership, MembershipFees, Authorization) |
|
||||||
| **Relationships** | 3 |
|
| **Relationships** | 7 |
|
||||||
| **Indexes** | 15+ |
|
| **Indexes** | 20+ |
|
||||||
| **Triggers** | 1 (Full-text search) |
|
| **Triggers** | 1 (Full-text search) |
|
||||||
|
|
||||||
## Tables Overview
|
## Tables Overview
|
||||||
|
|
@ -68,16 +68,39 @@ This document provides a comprehensive overview of the Mila Membership Managemen
|
||||||
- Immutable and required flags
|
- Immutable and required flags
|
||||||
- Centralized custom field management
|
- Centralized custom field management
|
||||||
|
|
||||||
|
#### `settings`
|
||||||
|
- **Purpose:** Global application settings (singleton resource)
|
||||||
|
- **Rows (Estimated):** 1 (singleton pattern)
|
||||||
|
- **Key Features:**
|
||||||
|
- Club name configuration
|
||||||
|
- Member field visibility settings
|
||||||
|
- Membership fee default settings
|
||||||
|
- Environment variable support for club name
|
||||||
|
|
||||||
|
### Authorization Domain
|
||||||
|
|
||||||
|
#### `roles`
|
||||||
|
- **Purpose:** Role-based access control (RBAC)
|
||||||
|
- **Rows (Estimated):** Low (typically 3-10 roles)
|
||||||
|
- **Key Features:**
|
||||||
|
- Links users to permission sets
|
||||||
|
- System role protection
|
||||||
|
- Four hardcoded permission sets: own_data, read_only, normal_user, admin
|
||||||
|
|
||||||
## Key Relationships
|
## Key Relationships
|
||||||
|
|
||||||
```
|
```
|
||||||
User (0..1) ←→ (0..1) Member
|
User (0..1) ←→ (0..1) Member
|
||||||
↓
|
↓ ↓
|
||||||
Tokens (N)
|
Tokens (N) CustomFieldValues (N)
|
||||||
|
↓ ↓
|
||||||
|
Role (N:1) CustomField (1)
|
||||||
|
|
||||||
Member (1) → (N) Properties
|
Member (1) → (N) MembershipFeeCycles
|
||||||
↓
|
↓
|
||||||
CustomField (1)
|
MembershipFeeType (1)
|
||||||
|
|
||||||
|
Settings (1) → MembershipFeeType (0..1)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Relationship Details
|
### Relationship Details
|
||||||
|
|
@ -89,16 +112,39 @@ Member (1) → (N) Properties
|
||||||
- Email synchronization when linked (User.email is source of truth)
|
- Email synchronization when linked (User.email is source of truth)
|
||||||
- `ON DELETE SET NULL` on user side (User preserved when Member deleted)
|
- `ON DELETE SET NULL` on user side (User preserved when Member deleted)
|
||||||
|
|
||||||
2. **Member → Properties (1:N)**
|
2. **User → Role (N:1)**
|
||||||
|
- Many users can be assigned to one role
|
||||||
|
- `ON DELETE RESTRICT` - cannot delete role if users are assigned
|
||||||
|
- Role links user to permission set for authorization
|
||||||
|
|
||||||
|
3. **Member → CustomFieldValues (1:N)**
|
||||||
- One member, many custom_field_values
|
- One member, many custom_field_values
|
||||||
- `ON DELETE CASCADE` - custom_field_values deleted with member
|
- `ON DELETE CASCADE` - custom_field_values deleted with member
|
||||||
- Composite unique constraint (member_id, custom_field_id)
|
- Composite unique constraint (member_id, custom_field_id)
|
||||||
|
|
||||||
3. **CustomFieldValue → CustomField (N:1)**
|
4. **CustomFieldValue → CustomField (N:1)**
|
||||||
- Properties reference type definition
|
- Custom field values reference type definition
|
||||||
- `ON DELETE RESTRICT` - cannot delete type if in use
|
- `ON DELETE RESTRICT` - cannot delete type if in use
|
||||||
- Type defines data structure
|
- Type defines data structure
|
||||||
|
|
||||||
|
5. **Member → MembershipFeeType (N:1, optional)**
|
||||||
|
- Many members can be assigned to one fee type
|
||||||
|
- `ON DELETE RESTRICT` - cannot delete fee type if members are assigned
|
||||||
|
- Optional relationship (member can have no fee type)
|
||||||
|
|
||||||
|
6. **Member → MembershipFeeCycles (1:N)**
|
||||||
|
- One member, many billing cycles
|
||||||
|
- `ON DELETE CASCADE` - cycles deleted when member deleted
|
||||||
|
- Unique constraint (member_id, cycle_start)
|
||||||
|
|
||||||
|
7. **MembershipFeeCycle → MembershipFeeType (N:1)**
|
||||||
|
- Many cycles reference one fee type
|
||||||
|
- `ON DELETE RESTRICT` - cannot delete fee type if cycles exist
|
||||||
|
|
||||||
|
8. **Settings → MembershipFeeType (N:1, optional)**
|
||||||
|
- Settings can reference a default fee type
|
||||||
|
- `ON DELETE SET NULL` - if fee type is deleted, setting is cleared
|
||||||
|
|
||||||
## Important Business Rules
|
## Important Business Rules
|
||||||
|
|
||||||
### Email Synchronization
|
### Email Synchronization
|
||||||
|
|
@ -141,7 +187,6 @@ Member (1) → (N) Properties
|
||||||
- `email` (B-tree) - Exact email lookups
|
- `email` (B-tree) - Exact email lookups
|
||||||
- `last_name` (B-tree) - Name sorting
|
- `last_name` (B-tree) - Name sorting
|
||||||
- `join_date` (B-tree) - Date filtering
|
- `join_date` (B-tree) - Date filtering
|
||||||
- `paid` (partial B-tree) - Payment status queries
|
|
||||||
|
|
||||||
**custom_field_values:**
|
**custom_field_values:**
|
||||||
- `member_id` - Member custom field value lookups
|
- `member_id` - Member custom field value lookups
|
||||||
|
|
@ -168,14 +213,14 @@ Member (1) → (N) Properties
|
||||||
### Weighted Fields
|
### Weighted Fields
|
||||||
- **Weight A (highest):** first_name, last_name
|
- **Weight A (highest):** first_name, last_name
|
||||||
- **Weight B:** email, notes
|
- **Weight B:** email, notes
|
||||||
- **Weight C:** phone_number, city, street, house_number, postal_code, custom_field_values
|
- **Weight C:** city, street, house_number, postal_code, custom_field_values
|
||||||
- **Weight D (lowest):** join_date, exit_date
|
- **Weight D (lowest):** join_date, exit_date
|
||||||
|
|
||||||
### Custom Field Values in Search
|
### Custom Field Values in Search
|
||||||
Custom field values are automatically included in the search vector:
|
Custom field values are automatically included in the search vector:
|
||||||
- All custom field values (string, integer, boolean, date, email) are aggregated and added to the search vector
|
- All custom field values (string, integer, boolean, date, email) are aggregated and added to the search vector
|
||||||
- Values are converted to text format for indexing
|
- Values are converted to text format for indexing
|
||||||
- Custom field values receive weight 'C' (same as phone_number, city, etc.)
|
- Custom field values receive weight 'C' (same as city, etc.)
|
||||||
- The search vector is automatically updated when custom field values are created, updated, or deleted via database triggers
|
- The search vector is automatically updated when custom field values are created, updated, or deleted via database triggers
|
||||||
|
|
||||||
### Usage Example
|
### Usage Example
|
||||||
|
|
@ -331,7 +376,7 @@ priv/repo/migrations/
|
||||||
|
|
||||||
**High Frequency:**
|
**High Frequency:**
|
||||||
- Member search (uses GIN index on search_vector)
|
- Member search (uses GIN index on search_vector)
|
||||||
- Member list with filters (uses indexes on join_date, paid)
|
- Member list with filters (uses indexes on join_date, membership_fee_type_id)
|
||||||
- User authentication (uses unique index on email/oidc_id)
|
- User authentication (uses unique index on email/oidc_id)
|
||||||
- CustomFieldValue lookups by member (uses index on member_id)
|
- CustomFieldValue lookups by member (uses index on member_id)
|
||||||
|
|
||||||
|
|
@ -350,7 +395,7 @@ priv/repo/migrations/
|
||||||
1. **Use indexes:** All critical query paths have indexes
|
1. **Use indexes:** All critical query paths have indexes
|
||||||
2. **Preload relationships:** Use Ash's `load` to avoid N+1
|
2. **Preload relationships:** Use Ash's `load` to avoid N+1
|
||||||
3. **Pagination:** Use keyset pagination (configured by default)
|
3. **Pagination:** Use keyset pagination (configured by default)
|
||||||
4. **Partial indexes:** `members.paid` index only non-NULL values
|
4. **GIN indexes:** Full-text search and fuzzy search on multiple fields
|
||||||
5. **Search optimization:** Full-text search via tsvector, not LIKE
|
5. **Search optimization:** Full-text search via tsvector, not LIKE
|
||||||
|
|
||||||
## Visualization
|
## Visualization
|
||||||
|
|
@ -464,7 +509,7 @@ mix run priv/repo/seeds.exs
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated:** 2025-11-13
|
**Last Updated:** 2026-01-13
|
||||||
**Schema Version:** 1.1
|
**Schema Version:** 1.4
|
||||||
**Database:** PostgreSQL 17.6 (dev) / 16 (prod)
|
**Database:** PostgreSQL 17.6 (dev) / 16 (prod)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
// - https://dbdocs.io
|
// - https://dbdocs.io
|
||||||
// - VS Code Extensions: "DBML Language" or "dbdiagram.io"
|
// - VS Code Extensions: "DBML Language" or "dbdiagram.io"
|
||||||
//
|
//
|
||||||
// Version: 1.3
|
// Version: 1.4
|
||||||
// Last Updated: 2025-12-11
|
// Last Updated: 2026-01-13
|
||||||
|
|
||||||
Project mila_membership_management {
|
Project mila_membership_management {
|
||||||
database_type: 'PostgreSQL'
|
database_type: 'PostgreSQL'
|
||||||
|
|
@ -28,6 +28,7 @@ Project mila_membership_management {
|
||||||
- **Accounts**: User authentication and session management
|
- **Accounts**: User authentication and session management
|
||||||
- **Membership**: Club member data and custom fields
|
- **Membership**: Club member data and custom fields
|
||||||
- **MembershipFees**: Membership fee types and billing cycles
|
- **MembershipFees**: Membership fee types and billing cycles
|
||||||
|
- **Authorization**: Role-based access control (RBAC)
|
||||||
|
|
||||||
## Required PostgreSQL Extensions:
|
## Required PostgreSQL Extensions:
|
||||||
- uuid-ossp (UUID generation)
|
- uuid-ossp (UUID generation)
|
||||||
|
|
@ -120,11 +121,9 @@ Table tokens {
|
||||||
|
|
||||||
Table members {
|
Table members {
|
||||||
id uuid [pk, not null, default: `uuid_generate_v7()`, note: 'UUIDv7 primary key (sortable by creation time)']
|
id uuid [pk, not null, default: `uuid_generate_v7()`, note: 'UUIDv7 primary key (sortable by creation time)']
|
||||||
first_name text [not null, note: 'Member first name (min length: 1)']
|
first_name text [null, note: 'Member first name (min length: 1 if present)']
|
||||||
last_name text [not null, note: 'Member last name (min length: 1)']
|
last_name text [null, note: 'Member last name (min length: 1 if present)']
|
||||||
email text [not null, unique, note: 'Member email address (5-254 chars, validated)']
|
email text [not null, unique, note: 'Member email address (5-254 chars, validated)']
|
||||||
paid boolean [null, note: 'Payment status flag']
|
|
||||||
phone_number text [null, note: 'Contact phone number (format: +?[0-9\- ]{6,20})']
|
|
||||||
join_date date [null, note: 'Date when member joined club (cannot be in future)']
|
join_date date [null, note: 'Date when member joined club (cannot be in future)']
|
||||||
exit_date date [null, note: 'Date when member left club (must be after join_date)']
|
exit_date date [null, note: 'Date when member left club (must be after join_date)']
|
||||||
notes text [null, note: 'Additional notes about member']
|
notes text [null, note: 'Additional notes about member']
|
||||||
|
|
@ -148,7 +147,6 @@ Table members {
|
||||||
email [name: 'members_email_idx', note: 'B-tree index for exact lookups']
|
email [name: 'members_email_idx', note: 'B-tree index for exact lookups']
|
||||||
last_name [name: 'members_last_name_idx', note: 'B-tree index for name sorting']
|
last_name [name: 'members_last_name_idx', note: 'B-tree index for name sorting']
|
||||||
join_date [name: 'members_join_date_idx', note: 'B-tree index for date filters']
|
join_date [name: 'members_join_date_idx', note: 'B-tree index for date filters']
|
||||||
(paid) [name: 'members_paid_idx', type: btree, note: 'Partial index WHERE paid IS NOT NULL']
|
|
||||||
membership_fee_type_id [name: 'members_membership_fee_type_id_index', note: 'B-tree index for fee type lookups']
|
membership_fee_type_id [name: 'members_membership_fee_type_id_index', note: 'B-tree index for fee type lookups']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,8 +155,8 @@ Table members {
|
||||||
|
|
||||||
Core entity for membership management containing:
|
Core entity for membership management containing:
|
||||||
- Personal information (name, email)
|
- Personal information (name, email)
|
||||||
- Contact details (phone, address)
|
- Contact details (address)
|
||||||
- Membership status (join/exit dates, payment status)
|
- Membership status (join/exit dates, membership fee cycles)
|
||||||
- Additional notes
|
- Additional notes
|
||||||
|
|
||||||
**Email Synchronization:**
|
**Email Synchronization:**
|
||||||
|
|
@ -186,12 +184,11 @@ Table members {
|
||||||
- 1:N with membership_fee_cycles - billing history
|
- 1:N with membership_fee_cycles - billing history
|
||||||
|
|
||||||
**Validation Rules:**
|
**Validation Rules:**
|
||||||
- first_name, last_name: min 1 character
|
- first_name, last_name: optional, but if present min 1 character
|
||||||
- email: 5-254 characters, valid email format
|
- email: 5-254 characters, valid email format (required)
|
||||||
- join_date: cannot be in future
|
- join_date: cannot be in future
|
||||||
- exit_date: must be after join_date (if both present)
|
- exit_date: must be after join_date (if both present)
|
||||||
- phone_number: matches pattern ^\+?[0-9\- ]{6,20}$
|
- postal_code: exactly 5 digits (if present)
|
||||||
- postal_code: exactly 5 digits
|
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -500,3 +497,138 @@ TableGroup membership_fees_domain {
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// AUTHORIZATION DOMAIN
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
Table roles {
|
||||||
|
id uuid [pk, not null, default: `uuid_generate_v7()`, note: 'UUIDv7 primary key']
|
||||||
|
name text [not null, unique, note: 'Unique role name (e.g., "Vorstand", "Admin", "Mitglied")']
|
||||||
|
description text [null, note: 'Human-readable description of the role']
|
||||||
|
permission_set_name text [not null, note: 'Permission set name: "own_data", "read_only", "normal_user", or "admin"']
|
||||||
|
is_system_role boolean [not null, default: false, note: 'If true, role cannot be deleted (protects critical roles)']
|
||||||
|
inserted_at timestamp [not null, default: `now() AT TIME ZONE 'utc'`, note: 'Creation timestamp (UTC)']
|
||||||
|
updated_at timestamp [not null, default: `now() AT TIME ZONE 'utc'`, note: 'Last update timestamp (UTC)']
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
name [unique, name: 'roles_unique_name_index']
|
||||||
|
}
|
||||||
|
|
||||||
|
Note: '''
|
||||||
|
**Role-Based Access Control (RBAC)**
|
||||||
|
|
||||||
|
Roles link users to permission sets. Each role references one of four hardcoded
|
||||||
|
permission sets defined in the application code.
|
||||||
|
|
||||||
|
**Permission Sets:**
|
||||||
|
- `own_data`: Users can only access their own linked member data
|
||||||
|
- `read_only`: Users can read all data but cannot modify
|
||||||
|
- `normal_user`: Users can read and modify most data (standard permissions)
|
||||||
|
- `admin`: Full access to all features and settings
|
||||||
|
|
||||||
|
**System Roles:**
|
||||||
|
- System roles (is_system_role = true) cannot be deleted
|
||||||
|
- Protects critical roles like "Mitglied" (member) from accidental deletion
|
||||||
|
- Only set via seed scripts or internal actions
|
||||||
|
|
||||||
|
**Relationships:**
|
||||||
|
- 1:N with users - users assigned to this role
|
||||||
|
- ON DELETE RESTRICT: Cannot delete role if users are assigned
|
||||||
|
|
||||||
|
**Constraints:**
|
||||||
|
- `name` must be unique
|
||||||
|
- `permission_set_name` must be a valid permission set (validated in application)
|
||||||
|
- System roles cannot be deleted (enforced via validation)
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// MEMBERSHIP DOMAIN (Additional Tables)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
Table settings {
|
||||||
|
id uuid [pk, not null, default: `gen_random_uuid()`, note: 'Primary identifier']
|
||||||
|
club_name text [not null, note: 'The name of the association/club (min length: 1)']
|
||||||
|
member_field_visibility jsonb [null, note: 'Visibility configuration for member fields in overview (JSONB map)']
|
||||||
|
include_joining_cycle boolean [not null, default: true, note: 'Whether to include the joining cycle in membership fee generation']
|
||||||
|
default_membership_fee_type_id uuid [null, note: 'FK to membership_fee_types - default fee type for new members']
|
||||||
|
inserted_at timestamp [not null, default: `now() AT TIME ZONE 'utc'`, note: 'Creation timestamp (UTC)']
|
||||||
|
updated_at timestamp [not null, default: `now() AT TIME ZONE 'utc'`, note: 'Last update timestamp (UTC)']
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
default_membership_fee_type_id [name: 'settings_default_membership_fee_type_id_index', note: 'B-tree index for fee type lookups']
|
||||||
|
}
|
||||||
|
|
||||||
|
Note: '''
|
||||||
|
**Global Application Settings (Singleton Resource)**
|
||||||
|
|
||||||
|
Stores global configuration for the association/club. There should only ever
|
||||||
|
be one settings record in the database (singleton pattern).
|
||||||
|
|
||||||
|
**Attributes:**
|
||||||
|
- `club_name`: The name of the association/club (required, can be set via ASSOCIATION_NAME env var)
|
||||||
|
- `member_field_visibility`: JSONB map storing visibility configuration for member fields
|
||||||
|
(e.g., `{"street": false, "house_number": false}`). Fields not in the map default to `true`.
|
||||||
|
- `include_joining_cycle`: When true, members pay from their joining cycle. When false,
|
||||||
|
they pay from the next full cycle after joining.
|
||||||
|
- `default_membership_fee_type_id`: The membership fee type automatically assigned to
|
||||||
|
new members. Can be nil if no default is set.
|
||||||
|
|
||||||
|
**Singleton Pattern:**
|
||||||
|
- Only one settings record should exist
|
||||||
|
- Designed to be read and updated, not created/destroyed via normal CRUD
|
||||||
|
- Initial settings should be seeded
|
||||||
|
|
||||||
|
**Environment Variable Support:**
|
||||||
|
- `club_name` can be set via `ASSOCIATION_NAME` environment variable
|
||||||
|
- Database values always take precedence over environment variables
|
||||||
|
|
||||||
|
**Relationships:**
|
||||||
|
- Optional N:1 with membership_fee_types - default fee type for new members
|
||||||
|
- ON DELETE SET NULL: If default fee type is deleted, setting is cleared
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// RELATIONSHIPS (Additional)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// User → Role (N:1)
|
||||||
|
// - Many users can be assigned to one role
|
||||||
|
// - ON DELETE RESTRICT: Cannot delete role if users are assigned
|
||||||
|
Ref: users.role_id > roles.id [delete: restrict]
|
||||||
|
|
||||||
|
// Settings → MembershipFeeType (N:1, optional)
|
||||||
|
// - Settings can reference a default membership fee type
|
||||||
|
// - ON DELETE SET NULL: If fee type is deleted, setting is cleared
|
||||||
|
Ref: settings.default_membership_fee_type_id > membership_fee_types.id [delete: set null]
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// TABLE GROUPS (Updated)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
TableGroup authorization_domain {
|
||||||
|
roles
|
||||||
|
|
||||||
|
Note: '''
|
||||||
|
**Authorization Domain**
|
||||||
|
|
||||||
|
Handles role-based access control (RBAC) with hardcoded permission sets.
|
||||||
|
Roles link users to permission sets for authorization.
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
|
||||||
|
TableGroup membership_domain {
|
||||||
|
members
|
||||||
|
custom_field_values
|
||||||
|
custom_fields
|
||||||
|
settings
|
||||||
|
|
||||||
|
Note: '''
|
||||||
|
**Membership Domain**
|
||||||
|
|
||||||
|
Core business logic for club membership management.
|
||||||
|
Supports flexible, extensible member data model.
|
||||||
|
Includes global application settings (singleton).
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ mix phx.new mv --no-ecto --no-mailer
|
||||||
**Key decisions:**
|
**Key decisions:**
|
||||||
- **Elixir 1.18.3 + OTP 27**: Latest stable versions for performance
|
- **Elixir 1.18.3 + OTP 27**: Latest stable versions for performance
|
||||||
- **Ash Framework 3.0**: Declarative resource layer, reduces boilerplate
|
- **Ash Framework 3.0**: Declarative resource layer, reduces boilerplate
|
||||||
- **Phoenix LiveView 1.1**: Real-time UI without JavaScript complexity
|
- **Phoenix LiveView 1.1.0-rc.3**: Real-time UI without JavaScript complexity
|
||||||
- **Tailwind CSS 4.0**: Utility-first styling with custom build
|
- **Tailwind CSS 4.0**: Utility-first styling with custom build
|
||||||
- **PostgreSQL 17**: Advanced features (full-text search, JSONB, citext)
|
- **PostgreSQL 17**: Advanced features (full-text search, JSONB, citext)
|
||||||
- **Bandit**: Modern HTTP server, better than Cowboy for LiveView
|
- **Bandit**: Modern HTTP server, better than Cowboy for LiveView
|
||||||
|
|
@ -80,14 +80,15 @@ mix phx.new mv --no-ecto --no-mailer
|
||||||
**Versions pinned in `.tool-versions`:**
|
**Versions pinned in `.tool-versions`:**
|
||||||
- Elixir 1.18.3-otp-27
|
- Elixir 1.18.3-otp-27
|
||||||
- Erlang 27.3.4
|
- Erlang 27.3.4
|
||||||
- Just 1.43.0
|
- Just 1.46.0
|
||||||
|
|
||||||
#### 4. Database Setup
|
#### 4. Database Setup
|
||||||
|
|
||||||
**PostgreSQL Extensions:**
|
**PostgreSQL Extensions:**
|
||||||
```sql
|
```sql
|
||||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- UUID generation
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- UUID generation (via uuid_generate_v7 function)
|
||||||
CREATE EXTENSION IF NOT EXISTS "citext"; -- Case-insensitive text
|
CREATE EXTENSION IF NOT EXISTS "citext"; -- Case-insensitive text
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- Trigram-based fuzzy search
|
||||||
```
|
```
|
||||||
|
|
||||||
**Migration Strategy:**
|
**Migration Strategy:**
|
||||||
|
|
@ -468,7 +469,7 @@ end
|
||||||
- **Tailwind:** Utility-first, no custom CSS
|
- **Tailwind:** Utility-first, no custom CSS
|
||||||
- **DaisyUI:** Pre-built components, consistent design
|
- **DaisyUI:** Pre-built components, consistent design
|
||||||
- **Heroicons:** Icon library, inline SVG
|
- **Heroicons:** Icon library, inline SVG
|
||||||
- **Phoenix LiveView:** Server-rendered, minimal JavaScript
|
- **Phoenix LiveView 1.1.0-rc.3:** Server-rendered, minimal JavaScript
|
||||||
|
|
||||||
**Trade-offs:**
|
**Trade-offs:**
|
||||||
- Larger HTML (utility classes)
|
- Larger HTML (utility classes)
|
||||||
|
|
@ -598,14 +599,33 @@ end
|
||||||
|
|
||||||
#### Database Migrations
|
#### Database Migrations
|
||||||
|
|
||||||
**Key migrations in chronological order:**
|
**Key migrations in chronological order (26 total):**
|
||||||
1. `20250528163901_initial_migration.exs` - Core tables (members, custom_field_values, custom_fields)
|
1. `20250421101957_initialize_extensions_1.exs` - PostgreSQL extensions (uuid-ossp, citext, pg_trgm)
|
||||||
2. `20250617090641_member_fields.exs` - Member attributes expansion
|
2. `20250528163901_initial_migration.exs` - Core tables (members, custom_field_values, custom_fields - originally property_types/properties)
|
||||||
3. `20250620110850_add_accounts_domain.exs` - Users & tokens tables
|
3. `20250617090641_member_fields.exs` - Member attributes expansion
|
||||||
4. `20250912085235_AddSearchVectorToMembers.exs` - Full-text search (tsvector + GIN index)
|
4. `20250617132424_member_delete.exs` - Member deletion constraints
|
||||||
5. `20250926164519_member_relation.exs` - User-Member link (optional 1:1)
|
5. `20250620110849_add_accounts_domain_extensions.exs` - Accounts domain extensions
|
||||||
6. `20251001141005_add_trigram_to_members.exs` - Fuzzy search (pg_trgm + 6 GIN trigram indexes)
|
6. `20250620110850_add_accounts_domain.exs` - Users & tokens tables
|
||||||
7. `20251016130855_add_constraints_for_user_member_and_property.exs` - Email sync constraints
|
7. `20250912085235_AddSearchVectorToMembers.exs` - Full-text search (tsvector + GIN index)
|
||||||
|
8. `20250926164519_member_relation.exs` - User-Member link (optional 1:1)
|
||||||
|
9. `20250926180341_add_unique_email_to_members.exs` - Unique email constraint on members
|
||||||
|
10. `20251001141005_add_trigram_to_members.exs` - Fuzzy search (pg_trgm + 6 GIN trigram indexes)
|
||||||
|
11. `20251016130855_add_constraints_for_user_member_and_property.exs` - Email sync constraints
|
||||||
|
12. `20251113163600_rename_properties_to_custom_fields_extensions_1.exs` - Rename properties extensions
|
||||||
|
13. `20251113163602_rename_properties_to_custom_fields.exs` - Rename property_types → custom_fields, properties → custom_field_values
|
||||||
|
14. `20251113180429_add_slug_to_custom_fields.exs` - Add slug to custom fields
|
||||||
|
15. `20251113183538_change_custom_field_delete_cascade.exs` - Change delete cascade behavior
|
||||||
|
16. `20251119160509_add_show_in_overview_to_custom_fields.exs` - Add show_in_overview flag
|
||||||
|
17. `20251127134451_add_settings_table.exs` - Create settings table (singleton)
|
||||||
|
18. `20251201115939_add_member_field_visibility_to_settings.exs` - Add member_field_visibility JSONB to settings
|
||||||
|
19. `20251202145404_remove_birth_date_from_members.exs` - Remove birth_date field
|
||||||
|
20. `20251204123714_add_custom_field_values_to_search_vector.exs` - Include custom field values in search vector
|
||||||
|
21. `20251211151449_add_membership_fees_tables.exs` - Create membership_fee_types and membership_fee_cycles tables
|
||||||
|
22. `20251211172549_remove_immutable_from_custom_fields.exs` - Remove immutable flag from custom fields
|
||||||
|
23. `20251211195058_add_membership_fee_settings.exs` - Add membership fee settings to settings table
|
||||||
|
24. `20251218113900_remove_paid_from_members.exs` - Remove paid boolean from members (replaced by cycle status)
|
||||||
|
25. `20260102155350_remove_phone_number_and_make_fields_optional.exs` - Remove phone_number, make first_name/last_name optional
|
||||||
|
26. `20260106161215_add_authorization_domain.exs` - Create roles table and add role_id to users
|
||||||
|
|
||||||
**Learning:** Ash's code generation from resources ensures schema always matches code.
|
**Learning:** Ash's code generation from resources ensures schema always matches code.
|
||||||
|
|
||||||
|
|
@ -1562,7 +1582,7 @@ Effective workflow:
|
||||||
|
|
||||||
This project demonstrates a modern Phoenix application built with:
|
This project demonstrates a modern Phoenix application built with:
|
||||||
- ✅ **Ash Framework** for declarative resources and policies
|
- ✅ **Ash Framework** for declarative resources and policies
|
||||||
- ✅ **Phoenix LiveView** for real-time, server-rendered UI
|
- ✅ **Phoenix LiveView 1.1.0-rc.3** for real-time, server-rendered UI
|
||||||
- ✅ **Tailwind CSS + DaisyUI** for rapid UI development
|
- ✅ **Tailwind CSS + DaisyUI** for rapid UI development
|
||||||
- ✅ **PostgreSQL** with advanced features (full-text search, UUIDv7)
|
- ✅ **PostgreSQL** with advanced features (full-text search, UUIDv7)
|
||||||
- ✅ **Multi-strategy authentication** (Password + OIDC)
|
- ✅ **Multi-strategy authentication** (Password + OIDC)
|
||||||
|
|
@ -1570,15 +1590,19 @@ This project demonstrates a modern Phoenix application built with:
|
||||||
- ✅ **Flexible data model** (EAV pattern with union types)
|
- ✅ **Flexible data model** (EAV pattern with union types)
|
||||||
|
|
||||||
**Key Achievements:**
|
**Key Achievements:**
|
||||||
- 🎯 8 sprints completed
|
- 🎯 9+ sprints completed
|
||||||
- 🚀 82 pull requests merged
|
- 🚀 100+ pull requests merged
|
||||||
- ✅ Core features implemented (CRUD, search, auth, sync)
|
- ✅ Core features implemented (CRUD, search, auth, sync, membership fees, roles & permissions)
|
||||||
|
- ✅ Membership fees system (types, cycles, settings)
|
||||||
|
- ✅ Role-based access control (RBAC) with 4 permission sets
|
||||||
|
- ✅ Member field visibility settings
|
||||||
|
- ✅ Sidebar navigation (WCAG 2.1 AA compliant)
|
||||||
- 📚 Comprehensive documentation
|
- 📚 Comprehensive documentation
|
||||||
- 🔒 Security-focused (audits, validations, policies)
|
- 🔒 Security-focused (audits, validations, policies)
|
||||||
- 🐳 Docker-ready for self-hosting
|
- 🐳 Docker-ready for self-hosting
|
||||||
|
|
||||||
**Next Steps:**
|
**Next Steps:**
|
||||||
- Implement roles & permissions
|
- ✅ ~~Implement roles & permissions~~ - RBAC system implemented (2026-01-08)
|
||||||
- Add payment tracking
|
- Add payment tracking
|
||||||
- ✅ ~~Improve accessibility (WCAG 2.1 AA)~~ - Keyboard navigation implemented
|
- ✅ ~~Improve accessibility (WCAG 2.1 AA)~~ - Keyboard navigation implemented
|
||||||
- Member self-service portal
|
- Member self-service portal
|
||||||
|
|
@ -1586,8 +1610,150 @@ This project demonstrates a modern Phoenix application built with:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Document Version:** 1.3
|
## Recent Updates (2025-12-02 to 2026-01-13)
|
||||||
**Last Updated:** 2025-12-02
|
|
||||||
|
### Membership Fees System Implementation (2025-12-11 to 2025-12-26)
|
||||||
|
|
||||||
|
**PR #283:** *Membership Fee - Database Schema & Ash Domain Foundation* (closes #275)
|
||||||
|
- Created `Mv.MembershipFees` domain
|
||||||
|
- Added `MembershipFeeType` resource with intervals (monthly, quarterly, half_yearly, yearly)
|
||||||
|
- Added `MembershipFeeCycle` resource for individual billing cycles
|
||||||
|
- Database migrations for membership fee tables
|
||||||
|
|
||||||
|
**PR #284:** *Calendar Cycle Calculation Logic* (closes #276)
|
||||||
|
- Calendar-based cycle calculation module
|
||||||
|
- Support for different intervals
|
||||||
|
- Cycle start/end date calculations
|
||||||
|
- Integration with member joining dates
|
||||||
|
|
||||||
|
**PR #290:** *Cycle Generation System* (closes #277)
|
||||||
|
- Automatic cycle generation for members
|
||||||
|
- Cycle regeneration when fee type changes
|
||||||
|
- Integration with member lifecycle hooks
|
||||||
|
- Actor-based authorization for cycle operations
|
||||||
|
|
||||||
|
**PR #291:** *Membership Fee Type Resource & Settings* (closes #278)
|
||||||
|
- Membership fee type CRUD operations
|
||||||
|
- Global membership fee settings
|
||||||
|
- Default fee type assignment
|
||||||
|
- `include_joining_cycle` setting
|
||||||
|
|
||||||
|
**PR #294:** *Cycle Management & Member Integration* (closes #279)
|
||||||
|
- Member-fee type relationship
|
||||||
|
- Cycle status tracking (unpaid, paid, suspended)
|
||||||
|
- Member detail view integration
|
||||||
|
- Cycle regeneration on fee type change
|
||||||
|
|
||||||
|
**PR #304:** *Membership Fee 6 - UI Components & LiveViews* (closes #280)
|
||||||
|
- Membership fee type management LiveViews
|
||||||
|
- Membership fee settings LiveView
|
||||||
|
- Cycle display in member detail view
|
||||||
|
- Payment status indicators
|
||||||
|
|
||||||
|
### Custom Fields Enhancements (2025-12-11 to 2026-01-02)
|
||||||
|
|
||||||
|
**PR #266:** *Implements search for custom fields* (closes #196)
|
||||||
|
- Custom field search in member overview
|
||||||
|
- Integration with full-text search
|
||||||
|
- Custom field value filtering
|
||||||
|
|
||||||
|
**PR #301:** *Implements validation for required custom fields* (closes #274)
|
||||||
|
- Required custom field validation
|
||||||
|
- Form-level validation
|
||||||
|
- Error messages for missing required fields
|
||||||
|
|
||||||
|
**PR #313:** *Fix hidden empty custom fields* (closes #282)
|
||||||
|
- Fixed display of empty custom fields
|
||||||
|
- Improved custom field visibility logic
|
||||||
|
|
||||||
|
### UI/UX Improvements (2025-12-03 to 2025-12-16)
|
||||||
|
|
||||||
|
**PR #240:** *Implement dropdown to show/hide columns in member overview* (closes #209)
|
||||||
|
- Field visibility dropdown
|
||||||
|
- User-specific field selection
|
||||||
|
- Integration with global settings
|
||||||
|
|
||||||
|
**PR #247:** *Visual hierarchy for fields in member view and edit form* (closes #231)
|
||||||
|
- Improved field grouping
|
||||||
|
- Visual hierarchy improvements
|
||||||
|
- Better form layout
|
||||||
|
|
||||||
|
**PR #250:** *UX - Avoid opening member by clicking the checkbox* (closes #233)
|
||||||
|
- Checkbox click handling
|
||||||
|
- Prevented accidental navigation
|
||||||
|
- Improved selection UX
|
||||||
|
|
||||||
|
**PR #259:** *Fix small UI issues* (closes #220)
|
||||||
|
- Various UI bug fixes
|
||||||
|
- Accessibility improvements
|
||||||
|
|
||||||
|
**PR #293:** *Small UX fixes* (closes #281)
|
||||||
|
- Additional UX improvements
|
||||||
|
- Polish and refinement
|
||||||
|
|
||||||
|
**PR #319:** *Reduce member fields* (closes #273)
|
||||||
|
- Removed unnecessary member fields
|
||||||
|
- Streamlined member data model
|
||||||
|
- Migration for field removal
|
||||||
|
|
||||||
|
### Roles and Permissions System (2026-01-06 to 2026-01-08)
|
||||||
|
- ✅ **RBAC Implementation Complete** - Member Resource Policies (#345)
|
||||||
|
- Four hardcoded permission sets: `own_data`, `read_only`, `normal_user`, `admin`
|
||||||
|
- Role-based access control with database-backed roles
|
||||||
|
- Member resource policies with scope filtering (`:own`, `:linked`, `:all`)
|
||||||
|
- Authorization checks via `Mv.Authorization.Checks.HasPermission`
|
||||||
|
- System role protection (cannot delete critical roles)
|
||||||
|
- Comprehensive test coverage
|
||||||
|
|
||||||
|
### Actor Handling Refactoring (2026-01-09)
|
||||||
|
- ✅ **Consistent Actor Access** - `current_actor/1` helper function
|
||||||
|
- Standardized actor access across all LiveViews
|
||||||
|
- `ash_actor_opts/1` helper for consistent authorization options
|
||||||
|
- `submit_form/3` wrapper for form submissions with actor
|
||||||
|
- All Ash operations now properly pass `actor` parameter
|
||||||
|
- Error handling improvements (replaced bang calls with proper error handling)
|
||||||
|
|
||||||
|
### Internationalization Improvements (2026-01-13)
|
||||||
|
- ✅ **Complete German Translations** - All UI strings translated
|
||||||
|
- CI check for empty German translations in lint task
|
||||||
|
- Standardized English `msgstr` entries (all empty for consistency)
|
||||||
|
- Corrected language headers in `.po` files
|
||||||
|
- Added missing translations for error messages
|
||||||
|
|
||||||
|
### Code Quality Improvements (2026-01-13)
|
||||||
|
- ✅ **Error Handling** - Replaced `Ash.read!` with proper error handling
|
||||||
|
- ✅ **Code Complexity** - Reduced nesting depth in `UserLive.Form`
|
||||||
|
- ✅ **Test Infrastructure** - Role tag support in `ConnCase`
|
||||||
|
|
||||||
|
### CSV Import Feature (2026-01-13)
|
||||||
|
- ✅ **CSV Templates** - Member import templates (#329)
|
||||||
|
- German and English CSV templates
|
||||||
|
- Template files in `priv/static/templates/`
|
||||||
|
|
||||||
|
### Sidebar Implementation (2026-01-12)
|
||||||
|
- ✅ **Sidebar Navigation** - Replaced navbar with sidebar (#260)
|
||||||
|
- Standard-compliant sidebar with comprehensive tests
|
||||||
|
- DaisyUI drawer pattern implementation
|
||||||
|
- Desktop expanded/collapsed states
|
||||||
|
- Mobile overlay drawer
|
||||||
|
- localStorage persistence for sidebar state
|
||||||
|
- WCAG 2.1 Level AA compliant
|
||||||
|
|
||||||
|
### Member Field Settings (2026-01-12, PR #300, closes #223)
|
||||||
|
- ✅ **Member Field Visibility Configuration** - Global settings for field visibility
|
||||||
|
- JSONB-based visibility configuration in Settings resource
|
||||||
|
- Per-field visibility toggle (show/hide in member overview)
|
||||||
|
- Atomic updates for single field visibility changes
|
||||||
|
- Integration with member list overview
|
||||||
|
- User-specific field selection (takes priority over global settings)
|
||||||
|
- Custom field visibility support
|
||||||
|
- Default visibility: all fields visible except `exit_date` (hidden by default)
|
||||||
|
- LiveComponent for managing member field visibility in settings page
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version:** 1.4
|
||||||
|
**Last Updated:** 2026-01-13
|
||||||
**Maintainer:** Development Team
|
**Maintainer:** Development Team
|
||||||
**Status:** Living Document (update as project evolves)
|
**Status:** Living Document (update as project evolves)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Feature Roadmap & Implementation Plan
|
# Feature Roadmap & Implementation Plan
|
||||||
|
|
||||||
**Project:** Mila - Membership Management System
|
**Project:** Mila - Membership Management System
|
||||||
**Last Updated:** 2025-11-10
|
**Last Updated:** 2026-01-13
|
||||||
**Status:** Planning Phase
|
**Status:** Active Development
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -37,17 +37,24 @@
|
||||||
- [#146](https://git.local-it.org/local-it/mitgliederverwaltung/issues/146) - Translate "or" in the login screen (Low)
|
- [#146](https://git.local-it.org/local-it/mitgliederverwaltung/issues/146) - Translate "or" in the login screen (Low)
|
||||||
- [#144](https://git.local-it.org/local-it/mitgliederverwaltung/issues/144) - Add language switch dropdown to login screen (Low)
|
- [#144](https://git.local-it.org/local-it/mitgliederverwaltung/issues/144) - Add language switch dropdown to login screen (Low)
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- ✅ **Role-based access control (RBAC)** - Implemented (2026-01-08, PR #346, closes #345)
|
||||||
|
- ✅ **Permission system** - Four hardcoded permission sets (`own_data`, `read_only`, `normal_user`, `admin`)
|
||||||
|
- ✅ **Database-backed roles** - Roles table with permission set references
|
||||||
|
- ✅ **Resource policies** - Member resource policies with scope filtering
|
||||||
|
- ✅ **Page-level authorization** - LiveView page access control
|
||||||
|
- ✅ **System role protection** - Critical roles cannot be deleted
|
||||||
|
|
||||||
**Missing Features:**
|
**Missing Features:**
|
||||||
- ❌ Role-based access control (RBAC)
|
|
||||||
- ❌ Permission system
|
|
||||||
- ❌ Password reset flow
|
- ❌ Password reset flow
|
||||||
- ❌ Email verification
|
- ❌ Email verification
|
||||||
- ❌ Two-factor authentication (future)
|
- ❌ Two-factor authentication (future)
|
||||||
|
|
||||||
**Related Issues:**
|
**Related Issues:**
|
||||||
- [#191](https://git.local-it.org/local-it/mitgliederverwaltung/issues/191) - Implement Roles in Ash (M)
|
- ✅ [#345](https://git.local-it.org/local-it/mitgliederverwaltung/issues/345) - Member Resource Policies (closed 2026-01-13)
|
||||||
- [#190](https://git.local-it.org/local-it/mitgliederverwaltung/issues/190) - Implement Permissions in Ash (M)
|
- ✅ [#191](https://git.local-it.org/local-it/mitgliederverwaltung/issues/191) - Implement Roles in Ash (M) - Completed
|
||||||
- [#151](https://git.local-it.org/local-it/mitgliederverwaltung/issues/151) - Define implementation plan for roles and permissions (M) [3/7 tasks done]
|
- ✅ [#190](https://git.local-it.org/local-it/mitgliederverwaltung/issues/190) - Implement Permissions in Ash (M) - Completed
|
||||||
|
- ✅ [#151](https://git.local-it.org/local-it/mitgliederverwaltung/issues/151) - Define implementation plan for roles and permissions (M) - Completed
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -187,23 +194,27 @@
|
||||||
|
|
||||||
**Current State:**
|
**Current State:**
|
||||||
- ✅ Basic "paid" boolean field on members
|
- ✅ Basic "paid" boolean field on members
|
||||||
- ✅ **UI Mock-ups for Membership Fee Types & Settings** (2025-12-02)
|
- ✅ **Membership Fee Types Management** - Full CRUD implementation
|
||||||
- ⚠️ No payment tracking
|
- ✅ **Membership Fee Cycles** - Individual billing cycles per member
|
||||||
|
- ✅ **Membership Fee Settings** - Global settings (include_joining_cycle, default_fee_type)
|
||||||
|
- ✅ **Cycle Generation** - Automatic cycle generation for members
|
||||||
|
- ✅ **Payment Status Tracking** - Status per cycle (unpaid, paid, suspended)
|
||||||
|
- ✅ **Member Fee Assignment** - Members can be assigned to fee types
|
||||||
|
- ✅ **Cycle Regeneration** - Regenerate cycles when fee type changes
|
||||||
|
- ✅ **UI Components** - Membership fee status in member list and detail views
|
||||||
|
|
||||||
**Open Issues:**
|
**Open Issues:**
|
||||||
- [#156](https://git.local-it.org/local-it/mitgliederverwaltung/issues/156) - Set up & document testing environment for vereinfacht.digital (L, Low priority)
|
- [#156](https://git.local-it.org/local-it/mitgliederverwaltung/issues/156) - Set up & document testing environment for vereinfacht.digital (L, Low priority)
|
||||||
- [#226](https://git.local-it.org/local-it/mitgliederverwaltung/issues/226) - Payment/Membership Fee Mockup Pages (Preview)
|
- ✅ [#226](https://git.local-it.org/local-it/mitgliederverwaltung/issues/226) - Payment/Membership Fee Mockup Pages (Preview) - Implemented
|
||||||
|
|
||||||
**Mock-Up Pages (Non-Functional Preview):**
|
**Implemented Pages:**
|
||||||
- `/membership_fee_types` - Membership Fee Types Management
|
- `/membership_fee_types` - Membership Fee Types Management (fully functional)
|
||||||
- `/membership_fee_settings` - Global Membership Fee Settings
|
- `/membership_fee_settings` - Global Membership Fee Settings (fully functional)
|
||||||
|
- `/members/:id` - Member detail view with membership fee cycles
|
||||||
|
|
||||||
**Missing Features:**
|
**Missing Features:**
|
||||||
- ❌ Membership fee configuration
|
- ❌ Payment records/transactions (external payment tracking)
|
||||||
- ❌ Payment records/transactions
|
|
||||||
- ❌ Payment history per member
|
|
||||||
- ❌ Payment reminders
|
- ❌ Payment reminders
|
||||||
- ❌ Payment status tracking (pending, paid, overdue)
|
|
||||||
- ❌ Invoice generation
|
- ❌ Invoice generation
|
||||||
- ❌ vereinfacht.digital API integration
|
- ❌ vereinfacht.digital API integration
|
||||||
- ❌ SEPA direct debit support
|
- ❌ SEPA direct debit support
|
||||||
|
|
@ -218,17 +229,18 @@
|
||||||
|
|
||||||
**Current State:**
|
**Current State:**
|
||||||
- ✅ AshAdmin integration (basic)
|
- ✅ AshAdmin integration (basic)
|
||||||
- ⚠️ No user-facing admin UI
|
- ✅ **Global Settings Management** - `/settings` page (singleton resource)
|
||||||
|
- ✅ **Club/Organization profile** - Club name configuration
|
||||||
|
- ✅ **Member Field Visibility Settings** - Configure which fields show in overview
|
||||||
|
- ✅ **CustomFieldValue type management UI** - Full CRUD for custom fields
|
||||||
|
- ✅ **Role Management UI** - Full CRUD for roles (`/admin/roles`)
|
||||||
|
- ✅ **Membership Fee Settings** - Global fee settings management
|
||||||
|
|
||||||
**Open Issues:**
|
**Open Issues:**
|
||||||
- [#186](https://git.local-it.org/local-it/mitgliederverwaltung/issues/186) - Create Architecture docs in Repo (S, Low priority)
|
- [#186](https://git.local-it.org/local-it/mitgliederverwaltung/issues/186) - Create Architecture docs in Repo (S, Low priority)
|
||||||
|
|
||||||
**Missing Features:**
|
**Missing Features:**
|
||||||
- ❌ Global settings management
|
|
||||||
- ❌ Club/Organization profile
|
|
||||||
- ❌ Email templates configuration
|
- ❌ Email templates configuration
|
||||||
- ❌ CustomFieldValue type management UI (user-facing)
|
|
||||||
- ❌ Role and permission management UI
|
|
||||||
- ❌ System health dashboard
|
- ❌ System health dashboard
|
||||||
- ❌ Audit log viewer
|
- ❌ Audit log viewer
|
||||||
- ❌ Backup/restore functionality
|
- ❌ Backup/restore functionality
|
||||||
|
|
@ -273,10 +285,12 @@
|
||||||
|
|
||||||
**Current State:**
|
**Current State:**
|
||||||
- ✅ Seed data script
|
- ✅ Seed data script
|
||||||
- ⚠️ No user-facing import/export
|
- ✅ **CSV Import Templates** - German and English templates (#329, 2026-01-13)
|
||||||
|
- Template files in `priv/static/templates/member_import_de.csv` and `member_import_en.csv`
|
||||||
|
- CSV specification documented in `docs/csv-member-import-v1.md`
|
||||||
|
|
||||||
**Missing Features:**
|
**Missing Features:**
|
||||||
- ❌ CSV import for members
|
- ❌ CSV import implementation (templates ready, import logic pending)
|
||||||
- ❌ Excel import for members
|
- ❌ Excel import for members
|
||||||
- ❌ Import validation and preview
|
- ❌ Import validation and preview
|
||||||
- ❌ Import error handling
|
- ❌ Import error handling
|
||||||
|
|
@ -452,6 +466,7 @@ Since this is a **Phoenix LiveView** application with **Ash Framework**, we have
|
||||||
| `GET` | `/auth/user/rauthy` | Initiate OIDC flow | 🔓 | - | Redirect to Rauthy |
|
| `GET` | `/auth/user/rauthy` | Initiate OIDC flow | 🔓 | - | Redirect to Rauthy |
|
||||||
| `GET` | `/auth/user/rauthy/callback` | Handle OIDC callback | 🔓 | `{code, state}` | Redirect + session cookie |
|
| `GET` | `/auth/user/rauthy/callback` | Handle OIDC callback | 🔓 | `{code, state}` | Redirect + session cookie |
|
||||||
| `POST` | `/auth/user/sign_out` | Sign out user | 🔐 | - | Redirect to login |
|
| `POST` | `/auth/user/sign_out` | Sign out user | 🔐 | - | Redirect to login |
|
||||||
|
| `GET` | `/auth/link-oidc-account` | OIDC account linking (password verification) | 🔓 | - | LiveView form | ✅ Implemented |
|
||||||
| `GET` | `/auth/user/password/reset` | Show password reset form | 🔓 | - | HTML form |
|
| `GET` | `/auth/user/password/reset` | Show password reset form | 🔓 | - | HTML form |
|
||||||
| `POST` | `/auth/user/password/reset` | Request password reset | 🔓 | `{email}` | Success message + email sent |
|
| `POST` | `/auth/user/password/reset` | Request password reset | 🔓 | `{email}` | Success message + email sent |
|
||||||
| `GET` | `/auth/user/password/reset/:token` | Show reset password form | 🔓 | - | HTML form |
|
| `GET` | `/auth/user/password/reset/:token` | Show reset password form | 🔓 | - | HTML form |
|
||||||
|
|
@ -537,13 +552,18 @@ Since this is a **Phoenix LiveView** application with **Ash Framework**, we have
|
||||||
|
|
||||||
### 3. Custom Fields (CustomFieldValue System) Endpoints
|
### 3. Custom Fields (CustomFieldValue System) Endpoints
|
||||||
|
|
||||||
#### LiveView Endpoints
|
#### LiveView Endpoints (✅ Implemented)
|
||||||
|
|
||||||
| Mount | Purpose | Auth | Events |
|
| Mount | Purpose | Auth | Events | Status |
|
||||||
|-------|---------|------|--------|
|
|-------|---------|------|--------|--------|
|
||||||
| `/custom-fields` | List custom fields | 🛡️ | `new`, `edit`, `delete` |
|
| `/settings` | Global settings (includes custom fields management) | 🔐 | `save`, `validate` | ✅ Implemented |
|
||||||
| `/custom-fields/new` | Create custom field | 🛡️ | `save`, `cancel` |
|
| `/custom_field_values` | List all custom field values | 🔐 | `new`, `edit`, `delete` | ✅ Implemented |
|
||||||
| `/custom-fields/:id/edit` | Edit custom field | 🛡️ | `save`, `cancel`, `delete` |
|
| `/custom_field_values/new` | Create custom field value | 🔐 | `save`, `cancel` | ✅ Implemented |
|
||||||
|
| `/custom_field_values/:id` | Custom field value detail | 🔐 | `edit` | ✅ Implemented |
|
||||||
|
| `/custom_field_values/:id/edit` | Edit custom field value | 🔐 | `save`, `cancel` | ✅ Implemented |
|
||||||
|
| `/custom_field_values/:id/show/edit` | Edit from show page | 🔐 | `save`, `cancel` | ✅ Implemented |
|
||||||
|
|
||||||
|
**Note:** Custom fields (definitions) are managed via LiveComponent in `/settings` page, not as separate routes.
|
||||||
|
|
||||||
#### Ash Resource Actions
|
#### Ash Resource Actions
|
||||||
|
|
||||||
|
|
@ -622,63 +642,81 @@ Since this is a **Phoenix LiveView** application with **Ash Framework**, we have
|
||||||
|
|
||||||
### 6. Internationalization Endpoints
|
### 6. Internationalization Endpoints
|
||||||
|
|
||||||
#### HTTP Controller Endpoints
|
#### HTTP Controller Endpoints (✅ Implemented)
|
||||||
|
|
||||||
| Method | Route | Purpose | Auth | Request | Response |
|
| Method | Route | Purpose | Auth | Request | Response | Status |
|
||||||
|--------|-------|---------|------|---------|----------|
|
|--------|-------|---------|------|---------|----------|--------|
|
||||||
| `POST` | `/locale` | Set user locale | 🔐 | `{locale: "de"}` | Redirect with cookie |
|
| `POST` | `/set_locale` | Set user locale | 🔐 | `{locale: "de"}` | Redirect with cookie | ✅ Implemented |
|
||||||
| `GET` | `/locales` | List available locales | 🔓 | - | `["de", "en"]` |
|
| `GET` | `/locales` | List available locales | 🔓 | - | `["de", "en"]` | ❌ Not implemented |
|
||||||
|
|
||||||
|
**Note:** Locale is set via `/set_locale` POST endpoint and persisted in session/cookie. Supported locales: `de` (default), `en`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 7. Payment & Fees Management Endpoints
|
### 7. Payment & Fees Management Endpoints
|
||||||
|
|
||||||
#### LiveView Endpoints (NEW - Issue #156)
|
#### LiveView Endpoints (✅ Implemented)
|
||||||
|
|
||||||
| Mount | Purpose | Auth | Events |
|
| Mount | Purpose | Auth | Events | Status |
|
||||||
|-------|---------|------|--------|
|
|-------|---------|------|--------|--------|
|
||||||
| `/payments` | Payment list | 🔐 | `new`, `record_payment`, `send_reminder` |
|
| `/membership_fee_types` | Membership fee type list | 🔐 | `new`, `edit`, `delete` | ✅ Implemented |
|
||||||
| `/payments/:id` | Payment detail | 🔐 | `edit`, `delete`, `mark_paid` |
|
| `/membership_fee_types/new` | Create membership fee type | 🔐 | `save`, `cancel` | ✅ Implemented |
|
||||||
| `/fees` | Fee configuration | 🛡️ | `create`, `edit`, `delete` |
|
| `/membership_fee_types/:id/edit` | Edit membership fee type | 🔐 | `save`, `cancel` | ✅ Implemented |
|
||||||
| `/invoices` | Invoice list | 🔐 | `generate`, `download`, `send` |
|
| `/membership_fee_settings` | Global membership fee settings | 🔐 | `save` | ✅ Implemented |
|
||||||
|
| `/contributions/member/:id` | Member contribution periods (mock-up) | 🔐 | - | ⚠️ Mock-up only |
|
||||||
|
| `/contribution_types` | Contribution types (mock-up) | 🔐 | - | ⚠️ Mock-up only |
|
||||||
|
|
||||||
#### Ash Resource Actions (NEW)
|
#### Ash Resource Actions (✅ Partially Implemented)
|
||||||
|
|
||||||
| Resource | Action | Purpose | Auth | Input | Output |
|
| Resource | Action | Purpose | Auth | Input | Output | Status |
|
||||||
|----------|--------|---------|------|-------|--------|
|
|----------|--------|---------|------|-------|--------|--------|
|
||||||
| `Fee` | `:create` | Create fee type | 🛡️ | `{name, amount, frequency}` | `{:ok, fee}` |
|
| `MembershipFeeType` | `:create` | Create fee type | 🔐 | `{name, amount, interval, ...}` | `{:ok, fee_type}` | ✅ Implemented |
|
||||||
| `Fee` | `:read` | List fees | 🔐 | - | `[%Fee{}]` |
|
| `MembershipFeeType` | `:read` | List fee types | 🔐 | - | `[%MembershipFeeType{}]` | ✅ Implemented |
|
||||||
| `Payment` | `:create` | Record payment | 🔐 | `{member_id, fee_id, amount, date}` | `{:ok, payment}` |
|
| `MembershipFeeType` | `:update` | Update fee type (name, amount, description) | 🔐 | `{id, attrs}` | `{:ok, fee_type}` | ✅ Implemented |
|
||||||
| `Payment` | `:list_by_member` | Member payment history | 🔐 | `{member_id}` | `[%Payment{}]` |
|
| `MembershipFeeType` | `:destroy` | Delete fee type (if no cycles) | 🔐 | `{id}` | `{:ok, fee_type}` | ✅ Implemented |
|
||||||
| `Payment` | `:mark_paid` | Mark as paid | 🔐 | `{id}` | `{:ok, payment}` |
|
| `MembershipFeeCycle` | `:read` | List cycles for member | 🔐 | `{member_id}` | `[%MembershipFeeCycle{}]` | ✅ Implemented |
|
||||||
| `Invoice` | `:generate` | Generate invoice | 🔐 | `{member_id, fee_id, period}` | `{:ok, invoice}` |
|
| `MembershipFeeCycle` | `:update` | Update cycle status | 🔐 | `{id, status}` | `{:ok, cycle}` | ✅ Implemented |
|
||||||
| `Invoice` | `:send` | Send invoice via email | 🔐 | `{id}` | `{:ok, sent}` |
|
| `Payment` | `:create` | Record payment | 🔐 | `{member_id, fee_id, amount, date}` | `{:ok, payment}` | ❌ Not implemented |
|
||||||
| `Payment` | `:import_vereinfacht` | Import from vereinfacht.digital | 🛡️ | `{transactions}` | `{:ok, count}` |
|
| `Payment` | `:list_by_member` | Member payment history | 🔐 | `{member_id}` | `[%Payment{}]` | ❌ Not implemented |
|
||||||
|
| `Payment` | `:mark_paid` | Mark as paid | 🔐 | `{id}` | `{:ok, payment}` | ❌ Not implemented |
|
||||||
|
| `Invoice` | `:generate` | Generate invoice | 🔐 | `{member_id, fee_id, period}` | `{:ok, invoice}` | ❌ Not implemented |
|
||||||
|
| `Invoice` | `:send` | Send invoice via email | 🔐 | `{id}` | `{:ok, sent}` | ❌ Not implemented |
|
||||||
|
| `Payment` | `:import_vereinfacht` | Import from vereinfacht.digital | 🛡️ | `{transactions}` | `{:ok, count}` | ❌ Not implemented |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 8. Admin Panel & Configuration Endpoints
|
### 8. Admin Panel & Configuration Endpoints
|
||||||
|
|
||||||
#### LiveView Endpoints (NEW)
|
#### LiveView Endpoints (✅ Partially Implemented)
|
||||||
|
|
||||||
| Mount | Purpose | Auth | Events |
|
| Mount | Purpose | Auth | Events | Status |
|
||||||
|-------|---------|------|--------|
|
|-------|---------|------|--------|--------|
|
||||||
| `/admin` | Admin dashboard | 🛡️ | - |
|
| `/settings` | Global settings (club name, member fields, custom fields) | 🔐 | `save`, `validate` | ✅ Implemented |
|
||||||
| `/admin/settings` | Global settings | 🛡️ | `save` |
|
| `/admin/roles` | Role management | 🛡️ | `new`, `edit`, `delete` | ✅ Implemented |
|
||||||
| `/admin/organization` | Organization profile | 🛡️ | `save` |
|
| `/admin/roles/new` | Create role | 🛡️ | `save`, `cancel` | ✅ Implemented |
|
||||||
| `/admin/email-templates` | Email template editor | 🛡️ | `create`, `edit`, `preview` |
|
| `/admin/roles/:id` | Role detail view | 🛡️ | `edit` | ✅ Implemented |
|
||||||
| `/admin/audit-log` | System audit log | 🛡️ | `filter`, `export` |
|
| `/admin/roles/:id/edit` | Edit role | 🛡️ | `save`, `cancel` | ✅ Implemented |
|
||||||
|
| `/admin` | Admin dashboard | 🛡️ | - | ❌ Not implemented |
|
||||||
|
| `/admin/organization` | Organization profile | 🛡️ | `save` | ❌ Not implemented |
|
||||||
|
| `/admin/email-templates` | Email template editor | 🛡️ | `create`, `edit`, `preview` | ❌ Not implemented |
|
||||||
|
| `/admin/audit-log` | System audit log | 🛡️ | `filter`, `export` | ❌ Not implemented |
|
||||||
|
|
||||||
#### Ash Resource Actions (NEW)
|
#### Ash Resource Actions (✅ Partially Implemented)
|
||||||
|
|
||||||
| Resource | Action | Purpose | Auth | Input | Output |
|
| Resource | Action | Purpose | Auth | Input | Output | Status |
|
||||||
|----------|--------|---------|------|-------|--------|
|
|----------|--------|---------|------|-------|--------|--------|
|
||||||
| `Setting` | `:get` | Get setting value | 🔐 | `{key}` | `value` |
|
| `Setting` | `:read` | Get settings (singleton) | 🔐 | - | `{:ok, settings}` | ✅ Implemented |
|
||||||
| `Setting` | `:set` | Set setting value | 🛡️ | `{key, value}` | `{:ok, setting}` |
|
| `Setting` | `:update` | Update settings | 🔐 | `{club_name, member_field_visibility, ...}` | `{:ok, settings}` | ✅ Implemented |
|
||||||
| `Setting` | `:list` | List all settings | 🛡️ | - | `[%Setting{}]` |
|
| `Setting` | `:update_member_field_visibility` | Update field visibility | 🔐 | `{member_field_visibility}` | `{:ok, settings}` | ✅ Implemented |
|
||||||
| `Organization` | `:read` | Get organization info | 🔐 | - | `%Organization{}` |
|
| `Setting` | `:update_single_member_field_visibility` | Atomic field visibility update | 🔐 | `{field, show_in_overview}` | `{:ok, settings}` | ✅ Implemented |
|
||||||
| `Organization` | `:update` | Update organization | 🛡️ | `{name, logo, ...}` | `{:ok, org}` |
|
| `Setting` | `:update_membership_fee_settings` | Update fee settings | 🔐 | `{include_joining_cycle, default_membership_fee_type_id}` | `{:ok, settings}` | ✅ Implemented |
|
||||||
| `AuditLog` | `:list` | List audit entries | 🛡️ | `{filters, pagination}` | `[%AuditLog{}]` |
|
| `Role` | `:read` | List roles | 🛡️ | - | `[%Role{}]` | ✅ Implemented |
|
||||||
|
| `Role` | `:create` | Create role | 🛡️ | `{name, permission_set_name, ...}` | `{:ok, role}` | ✅ Implemented |
|
||||||
|
| `Role` | `:update` | Update role | 🛡️ | `{id, attrs}` | `{:ok, role}` | ✅ Implemented |
|
||||||
|
| `Role` | `:destroy` | Delete role (if not system role) | 🛡️ | `{id}` | `{:ok, role}` | ✅ Implemented |
|
||||||
|
| `Organization` | `:read` | Get organization info | 🔐 | - | `%Organization{}` | ❌ Not implemented |
|
||||||
|
| `Organization` | `:update` | Update organization | 🛡️ | `{name, logo, ...}` | `{:ok, org}` | ❌ Not implemented |
|
||||||
|
| `AuditLog` | `:list` | List audit entries | 🛡️ | `{filters, pagination}` | `[%AuditLog{}]` | ❌ Not implemented |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
**Project:** Mila - Membership Management System
|
**Project:** Mila - Membership Management System
|
||||||
**Feature:** Membership Fee Management
|
**Feature:** Membership Fee Management
|
||||||
**Version:** 1.0
|
**Version:** 1.0
|
||||||
**Last Updated:** 2025-11-27
|
**Last Updated:** 2026-01-13
|
||||||
**Status:** Architecture Design - Ready for Implementation
|
**Status:** ✅ Implemented
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -76,6 +76,13 @@ This document defines the technical architecture for the Membership Fees system.
|
||||||
- `MembershipFeeType` - Membership fee type definitions (admin-managed)
|
- `MembershipFeeType` - Membership fee type definitions (admin-managed)
|
||||||
- `MembershipFeeCycle` - Individual membership fee cycles per member
|
- `MembershipFeeCycle` - Individual membership fee cycles per member
|
||||||
|
|
||||||
|
**Public API:**
|
||||||
|
The domain exposes code interface functions:
|
||||||
|
- `create_membership_fee_type/1`, `list_membership_fee_types/0`, `update_membership_fee_type/2`, `destroy_membership_fee_type/1`
|
||||||
|
- `create_membership_fee_cycle/1`, `list_membership_fee_cycles/0`, `update_membership_fee_cycle/2`, `destroy_membership_fee_cycle/1`
|
||||||
|
|
||||||
|
**Note:** In LiveViews, direct `Ash.read`, `Ash.create`, `Ash.update`, `Ash.destroy` calls are used with `domain: Mv.MembershipFees` instead of code interface functions. This is acceptable for LiveView forms that use `AshPhoenix.Form`.
|
||||||
|
|
||||||
**Extensions:**
|
**Extensions:**
|
||||||
|
|
||||||
- Member resource extended with membership fee fields
|
- Member resource extended with membership fee fields
|
||||||
|
|
@ -348,6 +355,9 @@ lib/
|
||||||
|
|
||||||
1. MembershipFeeType index/form (admin)
|
1. MembershipFeeType index/form (admin)
|
||||||
2. MembershipFeeCycle table component (member detail view)
|
2. MembershipFeeCycle table component (member detail view)
|
||||||
|
- Implemented as `MvWeb.MemberLive.Show.MembershipFeesComponent`
|
||||||
|
- Displays all cycles in a table with status management
|
||||||
|
- Allows changing cycle status, editing amounts, and regenerating cycles
|
||||||
3. Settings form section (admin)
|
3. Settings form section (admin)
|
||||||
4. Member list column (membership fee status)
|
4. Member list column (membership fee status)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
**Project:** Mila - Membership Management System
|
**Project:** Mila - Membership Management System
|
||||||
**Feature:** Membership Fee Management
|
**Feature:** Membership Fee Management
|
||||||
**Version:** 1.0
|
**Version:** 1.0
|
||||||
**Last Updated:** 2025-11-27
|
**Last Updated:** 2026-01-13
|
||||||
**Status:** Concept - Ready for Review
|
**Status:** ✅ Implemented
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
**Version:** 2.0 (Clean Rewrite)
|
**Version:** 2.0 (Clean Rewrite)
|
||||||
**Date:** 2025-01-13
|
**Date:** 2025-01-13
|
||||||
**Status:** Ready for Implementation
|
**Last Updated:** 2026-01-13
|
||||||
|
**Status:** ✅ Implemented (2026-01-08, PR #346, closes #345)
|
||||||
**Related Documents:**
|
**Related Documents:**
|
||||||
- [Overview](./roles-and-permissions-overview.md) - High-level concepts for stakeholders
|
- [Overview](./roles-and-permissions-overview.md) - High-level concepts for stakeholders
|
||||||
- [Implementation Plan](./roles-and-permissions-implementation-plan.md) - Step-by-step implementation guide
|
- [Implementation Plan](./roles-and-permissions-implementation-plan.md) - Step-by-step implementation guide
|
||||||
|
|
@ -1555,7 +1556,7 @@ end
|
||||||
**Navbar with conditional links:**
|
**Navbar with conditional links:**
|
||||||
|
|
||||||
```heex
|
```heex
|
||||||
<!-- lib/mv_web/components/layouts/navbar.html.heex -->
|
<!-- Note: Navbar has been replaced with Sidebar (lib/mv_web/components/layouts/sidebar.ex) -->
|
||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
<!-- Always visible -->
|
<!-- Always visible -->
|
||||||
<.link navigate="/">Home</.link>
|
<.link navigate="/">Home</.link>
|
||||||
|
|
@ -2484,7 +2485,8 @@ iex> MvWeb.Authorization.can_access_page?(user, "/members/new")
|
||||||
---
|
---
|
||||||
|
|
||||||
**Document Version:** 2.0 (Clean Rewrite)
|
**Document Version:** 2.0 (Clean Rewrite)
|
||||||
**Last Updated:** 2025-01-13
|
**Last Updated:** 2026-01-13
|
||||||
|
**Implementation Status:** ✅ Complete (2026-01-08)
|
||||||
**Status:** Ready for Implementation
|
**Status:** Ready for Implementation
|
||||||
|
|
||||||
**Changes from V1:**
|
**Changes from V1:**
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
**Version:** 2.0 (Clean Rewrite)
|
**Version:** 2.0 (Clean Rewrite)
|
||||||
**Date:** 2025-01-13
|
**Date:** 2025-01-13
|
||||||
**Status:** Ready for Implementation
|
**Last Updated:** 2026-01-13
|
||||||
|
**Status:** ✅ Implemented (2026-01-08, PR #346, closes #345)
|
||||||
**Related Documents:**
|
**Related Documents:**
|
||||||
- [Overview](./roles-and-permissions-overview.md) - High-level concepts
|
- [Overview](./roles-and-permissions-overview.md) - High-level concepts
|
||||||
- [Architecture](./roles-and-permissions-architecture.md) - Technical specification
|
- [Architecture](./roles-and-permissions-architecture.md) - Technical specification
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
**Project:** Mila - Membership Management System
|
**Project:** Mila - Membership Management System
|
||||||
**Feature:** Role-Based Access Control (RBAC) with Hardcoded Permission Sets
|
**Feature:** Role-Based Access Control (RBAC) with Hardcoded Permission Sets
|
||||||
**Version:** 2.0
|
**Version:** 2.0
|
||||||
**Last Updated:** 2025-11-13
|
**Last Updated:** 2026-01-13
|
||||||
**Status:** Architecture Design - MVP Approach
|
**Status:** ✅ Implemented (2026-01-08, PR #346, closes #345)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,747 +0,0 @@
|
||||||
# Sidebar Analysis - Current State
|
|
||||||
|
|
||||||
**Erstellt:** 2025-12-16
|
|
||||||
**Status:** Analyse für Neuimplementierung
|
|
||||||
**Autor:** Cursor AI Assistant
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
Die aktuelle Sidebar-Implementierung verwendet **nicht existierende Custom-CSS-Variants** (`is-drawer-close:` und `is-drawer-open:`), was zu einer defekten Implementierung führt. Die Sidebar ist strukturell basierend auf DaisyUI's Drawer-Komponente, aber die responsive und state-basierte Funktionalität ist nicht funktionsfähig.
|
|
||||||
|
|
||||||
**Kritisches Problem:** Die im Code verwendeten Variants `is-drawer-close:*` und `is-drawer-open:*` sind **nicht in Tailwind konfiguriert**, was bedeutet, dass diese Klassen beim Build ignoriert werden.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Dateien-Übersicht
|
|
||||||
|
|
||||||
### 1.1 Hauptdateien
|
|
||||||
|
|
||||||
| Datei | Zweck | Zeilen | Status |
|
|
||||||
|-------|-------|--------|--------|
|
|
||||||
| `lib/mv_web/components/layouts/sidebar.ex` | Sidebar-Komponente (Elixir) | 198 | ⚠️ Verwendet nicht existierende Variants |
|
|
||||||
| `lib/mv_web/components/layouts/navbar.ex` | Navbar mit Sidebar-Toggle | 48 | ✅ Funktional |
|
|
||||||
| `lib/mv_web/components/layouts.ex` | Layout-Wrapper mit Drawer | 121 | ✅ Funktional |
|
|
||||||
| `assets/js/app.js` | JavaScript für Sidebar-Interaktivität | 272 | ✅ Umfangreiche Accessibility-Logik |
|
|
||||||
| `assets/css/app.css` | CSS-Konfiguration | 103 | ⚠️ Keine Drawer-Variants definiert |
|
|
||||||
| `assets/tailwind.config.js` | Tailwind-Konfiguration | 75 | ⚠️ Keine Drawer-Variants definiert |
|
|
||||||
|
|
||||||
### 1.2 Verwandte Dateien
|
|
||||||
|
|
||||||
- `lib/mv_web/components/layouts/root.html.heex` - Root-Layout (minimal, keine Sidebar-Logik)
|
|
||||||
- `priv/static/images/logo.svg` - Logo (wird vermutlich für Sidebar benötigt)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Aktuelle Struktur
|
|
||||||
|
|
||||||
### 2.1 HTML-Struktur (DaisyUI Drawer Pattern)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- In layouts.ex -->
|
|
||||||
<div class="drawer">
|
|
||||||
<input id="main-drawer" type="checkbox" class="drawer-toggle" />
|
|
||||||
|
|
||||||
<div class="drawer-content">
|
|
||||||
<!-- Navbar mit Toggle-Button -->
|
|
||||||
<navbar with sidebar-toggle button />
|
|
||||||
|
|
||||||
<!-- Hauptinhalt -->
|
|
||||||
<main>...</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sidebar -->
|
|
||||||
<div class="drawer-side">
|
|
||||||
<button class="drawer-overlay" onclick="close drawer"></button>
|
|
||||||
<nav id="main-sidebar">
|
|
||||||
<!-- Navigation Items -->
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Bewertung:** ✅ Korrekte DaisyUI Drawer-Struktur
|
|
||||||
|
|
||||||
### 2.2 Sidebar-Komponente (`sidebar.ex`)
|
|
||||||
|
|
||||||
**Struktur:**
|
|
||||||
```elixir
|
|
||||||
defmodule MvWeb.Layouts.Sidebar do
|
|
||||||
attr :current_user, :map
|
|
||||||
attr :club_name, :string
|
|
||||||
|
|
||||||
def sidebar(assigns) do
|
|
||||||
# Rendert Sidebar mit Navigation, Locale-Selector, Theme-Toggle, User-Menu
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
**Hauptelemente:**
|
|
||||||
1. **Drawer Overlay** - Button zum Schließen (Mobile)
|
|
||||||
2. **Navigation Container** (`<nav id="main-sidebar">`)
|
|
||||||
3. **Menü-Items** - Members, Users, Contributions (nested), Settings
|
|
||||||
4. **Footer-Bereich** - Locale-Selector, Theme-Toggle, User-Menu
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Custom CSS Variants - KRITISCHES PROBLEM
|
|
||||||
|
|
||||||
### 3.1 Verwendete Variants im Code
|
|
||||||
|
|
||||||
Die Sidebar verwendet folgende Custom-Variants **extensiv**:
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
# Beispiele aus sidebar.ex
|
|
||||||
"is-drawer-close:overflow-visible"
|
|
||||||
"is-drawer-close:w-14 is-drawer-open:w-64"
|
|
||||||
"is-drawer-close:hidden"
|
|
||||||
"is-drawer-close:tooltip is-drawer-close:tooltip-right"
|
|
||||||
"is-drawer-close:w-auto"
|
|
||||||
"is-drawer-close:justify-center"
|
|
||||||
"is-drawer-close:dropdown-end"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Gefundene Verwendungen:**
|
|
||||||
- `is-drawer-close:` - 13 Instanzen in sidebar.ex
|
|
||||||
- `is-drawer-open:` - 1 Instanz in sidebar.ex
|
|
||||||
|
|
||||||
### 3.2 Definition der Variants
|
|
||||||
|
|
||||||
**❌ NICHT GEFUNDEN in:**
|
|
||||||
- `assets/css/app.css` - Enthält nur `phx-*-loading` Variants
|
|
||||||
- `assets/tailwind.config.js` - Enthält nur `phx-*-loading` Variants
|
|
||||||
|
|
||||||
**Fazit:** Diese Variants existieren **nicht** und werden beim Tailwind-Build **ignoriert**!
|
|
||||||
|
|
||||||
### 3.3 Vorhandene Variants
|
|
||||||
|
|
||||||
Nur folgende Custom-Variants sind tatsächlich definiert:
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* In app.css (Tailwind CSS 4.x Syntax) */
|
|
||||||
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
|
|
||||||
@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
|
|
||||||
@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
|
|
||||||
```
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
/* In tailwind.config.js (Tailwind 3.x Kompatibilität) */
|
|
||||||
plugin(({addVariant}) => addVariant("phx-click-loading", [...])),
|
|
||||||
plugin(({addVariant}) => addVariant("phx-submit-loading", [...])),
|
|
||||||
plugin(({addVariant}) => addVariant("phx-change-loading", [...])),
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. JavaScript-Implementierung
|
|
||||||
|
|
||||||
### 4.1 Übersicht
|
|
||||||
|
|
||||||
Die JavaScript-Implementierung ist **sehr umfangreich** und fokussiert auf Accessibility:
|
|
||||||
|
|
||||||
**Datei:** `assets/js/app.js` (Zeilen 106-270)
|
|
||||||
|
|
||||||
**Hauptfunktionalitäten:**
|
|
||||||
1. ✅ Tabindex-Management für fokussierbare Elemente
|
|
||||||
2. ✅ ARIA-Attribut-Management (`aria-expanded`)
|
|
||||||
3. ✅ Keyboard-Navigation (Enter, Space, Escape)
|
|
||||||
4. ✅ Focus-Management beim Öffnen/Schließen
|
|
||||||
5. ✅ Dropdown-Integration
|
|
||||||
|
|
||||||
### 4.2 Wichtige JavaScript-Funktionen
|
|
||||||
|
|
||||||
#### 4.2.1 Tabindex-Management
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const updateSidebarTabIndex = (isOpen) => {
|
|
||||||
const allFocusableElements = sidebar.querySelectorAll(
|
|
||||||
'a[href], button, select, input:not([type="hidden"]), [tabindex]'
|
|
||||||
)
|
|
||||||
|
|
||||||
allFocusableElements.forEach(el => {
|
|
||||||
if (isOpen) {
|
|
||||||
// Make focusable when open
|
|
||||||
el.removeAttribute('tabindex')
|
|
||||||
} else {
|
|
||||||
// Remove from tab order when closed
|
|
||||||
el.setAttribute('tabindex', '-1')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Zweck:** Verhindert, dass Nutzer mit Tab zu unsichtbaren Sidebar-Elementen springen können.
|
|
||||||
|
|
||||||
#### 4.2.2 ARIA-Expanded Management
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const updateAriaExpanded = () => {
|
|
||||||
const isOpen = drawerToggle.checked
|
|
||||||
sidebarToggle.setAttribute("aria-expanded", isOpen.toString())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Zweck:** Informiert Screen-Reader über den Sidebar-Status.
|
|
||||||
|
|
||||||
#### 4.2.3 Focus-Management
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const getFirstFocusableElement = () => {
|
|
||||||
// Priority: navigation link > other links > other focusable
|
|
||||||
const firstNavLink = sidebar.querySelector('a[href][role="menuitem"]')
|
|
||||||
// ... fallback logic
|
|
||||||
}
|
|
||||||
|
|
||||||
// On open: focus first element
|
|
||||||
// On close: focus toggle button
|
|
||||||
```
|
|
||||||
|
|
||||||
**Zweck:** Logische Fokus-Reihenfolge für Keyboard-Navigation.
|
|
||||||
|
|
||||||
#### 4.2.4 Keyboard-Shortcuts
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ESC to close
|
|
||||||
document.addEventListener("keydown", (e) => {
|
|
||||||
if (e.key === "Escape" && drawerToggle.checked) {
|
|
||||||
drawerToggle.checked = false
|
|
||||||
sidebarToggle.focus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Enter/Space on toggle button
|
|
||||||
sidebarToggle.addEventListener("keydown", (e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
// Toggle drawer and manage focus
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 LiveView Hooks
|
|
||||||
|
|
||||||
**Definierte Hooks:**
|
|
||||||
```javascript
|
|
||||||
Hooks.CopyToClipboard = { ... } // Clipboard-Funktionalität
|
|
||||||
Hooks.ComboBox = { ... } // Dropdown-Prävention bei Enter
|
|
||||||
```
|
|
||||||
|
|
||||||
**Sidebar-spezifisch:** Keine Hooks, nur native DOM-Events.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. DaisyUI Dependencies
|
|
||||||
|
|
||||||
### 5.1 Verwendete DaisyUI-Komponenten
|
|
||||||
|
|
||||||
| Komponente | Verwendung | Klassen |
|
|
||||||
|------------|-----------|---------|
|
|
||||||
| **Drawer** | Basis-Layout | `drawer`, `drawer-toggle`, `drawer-side`, `drawer-content`, `drawer-overlay` |
|
|
||||||
| **Menu** | Navigation | `menu`, `menu-title`, `w-64` |
|
|
||||||
| **Button** | Toggle, User-Menu | `btn`, `btn-ghost`, `btn-square`, `btn-circle` |
|
|
||||||
| **Avatar** | User-Menu | `avatar`, `avatar-placeholder` |
|
|
||||||
| **Dropdown** | User-Menu | `dropdown`, `dropdown-top`, `dropdown-end`, `dropdown-content` |
|
|
||||||
| **Tooltip** | Icon-Tooltips | `tooltip`, `tooltip-right` (via `data-tip`) |
|
|
||||||
| **Select** | Locale-Selector | `select`, `select-sm` |
|
|
||||||
| **Toggle** | Theme-Switch | `toggle`, `theme-controller` |
|
|
||||||
|
|
||||||
### 5.2 Standard Tailwind-Klassen
|
|
||||||
|
|
||||||
**Layout:**
|
|
||||||
- `flex`, `flex-col`, `items-start`, `justify-center`
|
|
||||||
- `gap-2`, `gap-4`, `p-4`, `mt-auto`, `w-full`, `w-64`, `min-h-full`
|
|
||||||
|
|
||||||
**Sizing:**
|
|
||||||
- `size-4`, `size-5`, `w-12`, `w-52`
|
|
||||||
|
|
||||||
**Colors:**
|
|
||||||
- `bg-base-100`, `bg-base-200`, `text-neutral-content`
|
|
||||||
|
|
||||||
**Typography:**
|
|
||||||
- `text-lg`, `text-sm`, `font-bold`
|
|
||||||
|
|
||||||
**Accessibility:**
|
|
||||||
- `sr-only`, `focus:outline-none`, `focus:ring-2`, `focus:ring-primary`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Toggle-Button (Navbar)
|
|
||||||
|
|
||||||
### 6.1 Implementierung
|
|
||||||
|
|
||||||
**Datei:** `lib/mv_web/components/layouts/navbar.ex`
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick="document.getElementById('main-drawer').checked = !document.getElementById('main-drawer').checked"
|
|
||||||
aria-label={gettext("Toggle navigation menu")}
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-controls="main-sidebar"
|
|
||||||
id="sidebar-toggle"
|
|
||||||
class="mr-2 btn btn-square btn-ghost"
|
|
||||||
>
|
|
||||||
<svg><!-- Layout-Panel-Left Icon --></svg>
|
|
||||||
</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Funktionalität:**
|
|
||||||
- ✅ Togglet Drawer-Checkbox
|
|
||||||
- ✅ ARIA-Labels vorhanden
|
|
||||||
- ✅ Keyboard-accessible
|
|
||||||
- ⚠️ `aria-expanded` wird durch JavaScript aktualisiert
|
|
||||||
|
|
||||||
**Icon:** Custom SVG (Layout-Panel-Left mit Chevron-Right)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Responsive Verhalten
|
|
||||||
|
|
||||||
### 7.1 Aktuelles Konzept (nicht funktional)
|
|
||||||
|
|
||||||
**Versuchte Implementierung:**
|
|
||||||
- **Desktop (collapsed):** Sidebar mit 14px Breite (`is-drawer-close:w-14`)
|
|
||||||
- **Desktop (expanded):** Sidebar mit 64px Breite (`is-drawer-open:w-64`)
|
|
||||||
- **Mobile:** Overlay-Drawer (DaisyUI Standard)
|
|
||||||
|
|
||||||
### 7.2 Problem
|
|
||||||
|
|
||||||
Da die `is-drawer-*` Variants nicht existieren, gibt es **kein responsives Verhalten**:
|
|
||||||
- Die Sidebar hat immer eine feste Breite von `w-64`
|
|
||||||
- Die conditional hiding (`:hidden`, etc.) funktioniert nicht
|
|
||||||
- Tooltips werden nicht conditional angezeigt
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Accessibility-Features
|
|
||||||
|
|
||||||
### 8.1 Implementierte Features
|
|
||||||
|
|
||||||
| Feature | Status | Implementierung |
|
|
||||||
|---------|--------|-----------------|
|
|
||||||
| **ARIA Labels** | ✅ | Alle interaktiven Elemente haben Labels |
|
|
||||||
| **ARIA Roles** | ✅ | `menubar`, `menuitem`, `menu`, `button` |
|
|
||||||
| **ARIA Expanded** | ✅ | Wird durch JS dynamisch gesetzt |
|
|
||||||
| **ARIA Controls** | ✅ | Toggle → Sidebar verknüpft |
|
|
||||||
| **Keyboard Navigation** | ✅ | Enter, Space, Escape, Tab |
|
|
||||||
| **Focus Management** | ✅ | Logische Focus-Reihenfolge |
|
|
||||||
| **Tabindex Management** | ✅ | Verhindert Focus auf hidden Elements |
|
|
||||||
| **Screen Reader Only** | ✅ | `.sr-only` für visuelle Labels |
|
|
||||||
| **Focus Indicators** | ✅ | `focus:ring-2 focus:ring-primary` |
|
|
||||||
| **Skip Links** | ❌ | Nicht vorhanden |
|
|
||||||
|
|
||||||
### 8.2 Accessibility-Score
|
|
||||||
|
|
||||||
**Geschätzt:** 90/100 (WCAG 2.1 Level AA konform)
|
|
||||||
|
|
||||||
**Verbesserungspotenzial:**
|
|
||||||
- Skip-Link zur Hauptnavigation hinzufügen
|
|
||||||
- High-Contrast-Mode testen
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Menü-Struktur
|
|
||||||
|
|
||||||
### 9.1 Navigation Items
|
|
||||||
|
|
||||||
```
|
|
||||||
📋 Main Menu
|
|
||||||
├── 👥 Members (/members)
|
|
||||||
├── 👤 Users (/users)
|
|
||||||
├── 💰 Contributions (collapsed submenu)
|
|
||||||
│ ├── Plans (/contribution_types)
|
|
||||||
│ └── Settings (/contribution_settings)
|
|
||||||
└── ⚙️ Settings (/settings)
|
|
||||||
|
|
||||||
🔽 Footer Area (logged in only)
|
|
||||||
├── 🌐 Locale Selector (DE/EN)
|
|
||||||
├── 🌓 Theme Toggle (Light/Dark)
|
|
||||||
└── 👤 User Menu (Dropdown)
|
|
||||||
├── Profile (/users/:id)
|
|
||||||
└── Logout (/sign-out)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9.2 Conditional Rendering
|
|
||||||
|
|
||||||
**Nicht eingeloggt:**
|
|
||||||
- Sidebar ist leer (nur Struktur)
|
|
||||||
- Keine Menü-Items
|
|
||||||
|
|
||||||
**Eingeloggt:**
|
|
||||||
- Vollständige Navigation
|
|
||||||
- Footer-Bereich mit User-Menu
|
|
||||||
|
|
||||||
### 9.3 Nested Menu (Contributions)
|
|
||||||
|
|
||||||
**Problem:** Das Contributions-Submenu ist **immer versteckt** im collapsed State:
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
<li class="is-drawer-close:hidden" role="none">
|
|
||||||
<h2 class="flex items-center gap-2 menu-title">
|
|
||||||
<.icon name="hero-currency-dollar" />
|
|
||||||
{gettext("Contributions")}
|
|
||||||
</h2>
|
|
||||||
<ul role="menu">
|
|
||||||
<li class="is-drawer-close:hidden">...</li>
|
|
||||||
<li class="is-drawer-close:hidden">...</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
```
|
|
||||||
|
|
||||||
Da `:hidden` nicht funktioniert, wird das Submenu immer angezeigt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Theme-Funktionalität
|
|
||||||
|
|
||||||
### 10.1 Theme-Toggle
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
value="dark"
|
|
||||||
class="toggle theme-controller"
|
|
||||||
aria-label={gettext("Toggle dark mode")}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Funktionalität:**
|
|
||||||
- ✅ DaisyUI `theme-controller` - automatische Theme-Umschaltung
|
|
||||||
- ✅ Persistence durch `localStorage` (siehe root.html.heex Script)
|
|
||||||
- ✅ Icon-Wechsel (Sun ↔ Moon)
|
|
||||||
|
|
||||||
### 10.2 Definierte Themes
|
|
||||||
|
|
||||||
**Datei:** `assets/css/app.css`
|
|
||||||
|
|
||||||
1. **Light Theme** (default)
|
|
||||||
- Base: `oklch(98% 0 0)`
|
|
||||||
- Primary: `oklch(70% 0.213 47.604)` (Orange/Phoenix-inspiriert)
|
|
||||||
|
|
||||||
2. **Dark Theme**
|
|
||||||
- Base: `oklch(30.33% 0.016 252.42)`
|
|
||||||
- Primary: `oklch(58% 0.233 277.117)` (Purple/Elixir-inspiriert)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Locale-Funktionalität
|
|
||||||
|
|
||||||
### 11.1 Locale-Selector
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
<form method="post" action="/set_locale">
|
|
||||||
<select
|
|
||||||
id="locale-select-sidebar"
|
|
||||||
name="locale"
|
|
||||||
onchange="this.form.submit()"
|
|
||||||
class="select select-sm w-full is-drawer-close:w-auto"
|
|
||||||
>
|
|
||||||
<option value="de">Deutsch</option>
|
|
||||||
<option value="en">English</option>
|
|
||||||
</select>
|
|
||||||
</form>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Funktionalität:**
|
|
||||||
- ✅ POST zu `/set_locale` Endpoint
|
|
||||||
- ✅ CSRF-Token included
|
|
||||||
- ✅ Auto-Submit on change
|
|
||||||
- ✅ Accessible Label (`.sr-only`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Probleme und Defekte
|
|
||||||
|
|
||||||
### 12.1 Kritische Probleme
|
|
||||||
|
|
||||||
| Problem | Schweregrad | Details |
|
|
||||||
|---------|-------------|---------|
|
|
||||||
| **Nicht existierende CSS-Variants** | 🔴 Kritisch | `is-drawer-close:*` und `is-drawer-open:*` sind nicht definiert |
|
|
||||||
| **Keine responsive Funktionalität** | 🔴 Kritisch | Sidebar verhält sich nicht wie geplant |
|
|
||||||
| **Conditional Styles funktionieren nicht** | 🔴 Kritisch | Hidden/Tooltip/Width-Changes werden ignoriert |
|
|
||||||
|
|
||||||
### 12.2 Mittlere Probleme
|
|
||||||
|
|
||||||
| Problem | Schweregrad | Details |
|
|
||||||
|---------|-------------|---------|
|
|
||||||
| **Kein Logo** | 🟡 Mittel | Logo-Element fehlt komplett in der Sidebar |
|
|
||||||
| **Submenu immer sichtbar** | 🟡 Mittel | Contributions-Submenu sollte in collapsed State versteckt sein |
|
|
||||||
| **Toggle-Icon statisch** | 🟡 Mittel | Icon ändert sich nicht zwischen expanded/collapsed |
|
|
||||||
|
|
||||||
### 12.3 Kleinere Probleme
|
|
||||||
|
|
||||||
| Problem | Schweregrad | Details |
|
|
||||||
|---------|-------------|---------|
|
|
||||||
| **Code-Redundanz** | 🟢 Klein | Variants in beiden Tailwind-Configs (3.x und 4.x) |
|
|
||||||
| **Inline-onclick Handler** | 🟢 Klein | Sollten durch JS-Events ersetzt werden |
|
|
||||||
| **Keine Skip-Links** | 🟢 Klein | Accessibility-Verbesserung |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13. Abhängigkeiten
|
|
||||||
|
|
||||||
### 13.1 Externe Abhängigkeiten
|
|
||||||
|
|
||||||
| Dependency | Version | Verwendung |
|
|
||||||
|------------|---------|------------|
|
|
||||||
| **DaisyUI** | Latest (vendor) | Drawer, Menu, Button, etc. |
|
|
||||||
| **Tailwind CSS** | 4.0.9 | Utility-Klassen |
|
|
||||||
| **Heroicons** | v2.2.0 | Icons in Navigation |
|
|
||||||
| **Phoenix LiveView** | ~> 1.1.0 | Backend-Integration |
|
|
||||||
|
|
||||||
### 13.2 Interne Abhängigkeiten
|
|
||||||
|
|
||||||
| Modul | Verwendung |
|
|
||||||
|-------|-----------|
|
|
||||||
| `MvWeb.Gettext` | Internationalisierung |
|
|
||||||
| `Mv.Membership.get_settings()` | Club-Name abrufen |
|
|
||||||
| `MvWeb.CoreComponents` | Icons, Links |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 14. Code-Qualität
|
|
||||||
|
|
||||||
### 14.1 Positives
|
|
||||||
|
|
||||||
- ✅ **Sehr gute Accessibility-Implementierung**
|
|
||||||
- ✅ **Saubere Modulstruktur** (Separation of Concerns)
|
|
||||||
- ✅ **Gute Dokumentation** (Moduledocs, Attribute docs)
|
|
||||||
- ✅ **Internationalisierung** vollständig implementiert
|
|
||||||
- ✅ **ARIA-Best-Practices** befolgt
|
|
||||||
- ✅ **Keyboard-Navigation** umfassend
|
|
||||||
|
|
||||||
### 14.2 Verbesserungsbedarf
|
|
||||||
|
|
||||||
- ❌ **Broken CSS-Variants** (Hauptproblem)
|
|
||||||
- ❌ **Fehlende Tests** (keine Component-Tests gefunden)
|
|
||||||
- ⚠️ **Inline-JavaScript** in onclick-Attributen
|
|
||||||
- ⚠️ **Magic-IDs** (`main-drawer`, `sidebar-toggle`) hardcoded
|
|
||||||
- ⚠️ **Komplexe JavaScript-Logik** ohne Dokumentation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 15. Empfehlungen für Neuimplementierung
|
|
||||||
|
|
||||||
### 15.1 Sofort-Maßnahmen
|
|
||||||
|
|
||||||
1. **CSS-Variants entfernen**
|
|
||||||
- Alle `is-drawer-close:*` und `is-drawer-open:*` entfernen
|
|
||||||
- Durch Standard-Tailwind oder DaisyUI-Mechanismen ersetzen
|
|
||||||
|
|
||||||
2. **Logo hinzufügen**
|
|
||||||
- Logo-Element als erstes Element in Sidebar
|
|
||||||
- Konsistente Größe (32px / size-8)
|
|
||||||
|
|
||||||
3. **Toggle-Icon implementieren**
|
|
||||||
- Icon-Swap zwischen Chevron-Left und Chevron-Right
|
|
||||||
- Nur auf Desktop sichtbar
|
|
||||||
|
|
||||||
### 15.2 Architektur-Entscheidungen
|
|
||||||
|
|
||||||
1. **Responsive Strategie:**
|
|
||||||
- **Mobile:** Standard DaisyUI Drawer (Overlay)
|
|
||||||
- **Desktop:** Persistent Sidebar mit fester Breite
|
|
||||||
- **Kein collapsing auf Desktop** (einfacher, wartbarer)
|
|
||||||
|
|
||||||
2. **State-Management:**
|
|
||||||
- Drawer-Checkbox für Mobile
|
|
||||||
- Keine zusätzlichen Custom-Variants
|
|
||||||
- Standard DaisyUI-Mechanismen verwenden
|
|
||||||
|
|
||||||
3. **JavaScript-Refactoring:**
|
|
||||||
- Hooks statt inline-onclick
|
|
||||||
- Dokumentierte Funktionen
|
|
||||||
- Unit-Tests für kritische Logik
|
|
||||||
|
|
||||||
### 15.3 Prioritäten
|
|
||||||
|
|
||||||
**High Priority:**
|
|
||||||
1. CSS-Variants-Problem lösen
|
|
||||||
2. Logo implementieren
|
|
||||||
3. Basic responsive Funktionalität
|
|
||||||
|
|
||||||
**Medium Priority:**
|
|
||||||
4. Toggle-Icon implementieren
|
|
||||||
5. Tests schreiben
|
|
||||||
6. JavaScript refactoren
|
|
||||||
|
|
||||||
**Low Priority:**
|
|
||||||
7. Skip-Links hinzufügen
|
|
||||||
8. Code-Optimierung
|
|
||||||
9. Performance-Tuning
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 16. Checkliste für Neuimplementierung
|
|
||||||
|
|
||||||
### 16.1 Vorbereitung
|
|
||||||
|
|
||||||
- [ ] Alle `is-drawer-*` Klassen aus Code entfernen
|
|
||||||
- [ ] Keine Custom-Variants in CSS/Tailwind definieren
|
|
||||||
- [ ] DaisyUI-Dokumentation für Drawer studieren
|
|
||||||
|
|
||||||
### 16.2 Implementation
|
|
||||||
|
|
||||||
- [ ] Logo-Element hinzufügen (size-8, persistent)
|
|
||||||
- [ ] Toggle-Button mit Icon-Swap (nur Desktop)
|
|
||||||
- [ ] Mobile: Overlay-Drawer (DaisyUI Standard)
|
|
||||||
- [ ] Desktop: Persistent Sidebar (w-64)
|
|
||||||
- [ ] Menü-Items mit korrekten Klassen
|
|
||||||
- [ ] Submenu-Handling (nested `<ul>`)
|
|
||||||
|
|
||||||
### 16.3 Funktionalität
|
|
||||||
|
|
||||||
- [ ] Toggle-Funktionalität auf Mobile
|
|
||||||
- [ ] Accessibility: ARIA, Focus, Keyboard
|
|
||||||
- [ ] Theme-Toggle funktional
|
|
||||||
- [ ] Locale-Selector funktional
|
|
||||||
- [ ] User-Menu-Dropdown funktional
|
|
||||||
|
|
||||||
### 16.4 Testing
|
|
||||||
|
|
||||||
- [ ] Component-Tests schreiben
|
|
||||||
- [ ] Accessibility-Tests (axe-core)
|
|
||||||
- [ ] Keyboard-Navigation testen
|
|
||||||
- [ ] Screen-Reader testen
|
|
||||||
- [ ] Responsive Breakpoints testen
|
|
||||||
|
|
||||||
### 16.5 Dokumentation
|
|
||||||
|
|
||||||
- [ ] Code-Kommentare aktualisieren
|
|
||||||
- [ ] Component-Docs schreiben
|
|
||||||
- [ ] README aktualisieren
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 17. Technische Details
|
|
||||||
|
|
||||||
### 17.1 CSS-Selektoren
|
|
||||||
|
|
||||||
**Verwendete IDs:**
|
|
||||||
- `#main-drawer` - Drawer-Toggle-Checkbox
|
|
||||||
- `#main-sidebar` - Sidebar-Navigation-Container
|
|
||||||
- `#sidebar-toggle` - Toggle-Button in Navbar
|
|
||||||
- `#locale-select-sidebar` - Locale-Dropdown
|
|
||||||
|
|
||||||
**Verwendete Klassen:**
|
|
||||||
- `.drawer-side` - DaisyUI Sidebar-Container
|
|
||||||
- `.drawer-overlay` - DaisyUI Overlay-Button
|
|
||||||
- `.drawer-content` - DaisyUI Content-Container
|
|
||||||
- `.menu` - DaisyUI Menu-Container
|
|
||||||
- `.is-drawer-close:*` - ❌ NICHT DEFINIERT
|
|
||||||
- `.is-drawer-open:*` - ❌ NICHT DEFINIERT
|
|
||||||
|
|
||||||
### 17.2 Event-Handler
|
|
||||||
|
|
||||||
**JavaScript:**
|
|
||||||
```javascript
|
|
||||||
drawerToggle.addEventListener("change", ...)
|
|
||||||
sidebarToggle.addEventListener("click", ...)
|
|
||||||
sidebarToggle.addEventListener("keydown", ...)
|
|
||||||
document.addEventListener("keydown", ...) // ESC handler
|
|
||||||
```
|
|
||||||
|
|
||||||
**Inline (zu migrieren):**
|
|
||||||
```elixir
|
|
||||||
onclick="document.getElementById('main-drawer').checked = false"
|
|
||||||
onclick="document.getElementById('main-drawer').checked = !..."
|
|
||||||
onchange="this.form.submit()"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 18. Metriken
|
|
||||||
|
|
||||||
### 18.1 Code-Metriken
|
|
||||||
|
|
||||||
| Metrik | Wert |
|
|
||||||
|--------|------|
|
|
||||||
| **Zeilen Code (Sidebar)** | 198 |
|
|
||||||
| **Zeilen JavaScript** | 165 (Sidebar-spezifisch) |
|
|
||||||
| **Zeilen CSS** | 0 (nur Tailwind-Klassen) |
|
|
||||||
| **Anzahl Komponenten** | 1 (Sidebar) + 1 (Navbar) |
|
|
||||||
| **Anzahl Menü-Items** | 6 (inkl. Submenu) |
|
|
||||||
| **Anzahl Footer-Controls** | 3 (Locale, Theme, User) |
|
|
||||||
|
|
||||||
### 18.2 Abhängigkeits-Metriken
|
|
||||||
|
|
||||||
| Kategorie | Anzahl |
|
|
||||||
|-----------|--------|
|
|
||||||
| **DaisyUI-Komponenten** | 7 |
|
|
||||||
| **Tailwind-Utility-Klassen** | ~50 |
|
|
||||||
| **Custom-Variants (broken)** | 2 (`is-drawer-close`, `is-drawer-open`) |
|
|
||||||
| **JavaScript-Event-Listener** | 6 |
|
|
||||||
| **ARIA-Attribute** | 12 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 19. Zusammenfassung
|
|
||||||
|
|
||||||
### 19.1 Was funktioniert
|
|
||||||
|
|
||||||
✅ **Sehr gute Grundlage:**
|
|
||||||
- DaisyUI Drawer-Pattern korrekt implementiert
|
|
||||||
- Exzellente Accessibility (ARIA, Keyboard, Focus)
|
|
||||||
- Saubere Modulstruktur
|
|
||||||
- Internationalisierung
|
|
||||||
- Theme-Switching
|
|
||||||
- JavaScript-Logik ist robust
|
|
||||||
|
|
||||||
### 19.2 Was nicht funktioniert
|
|
||||||
|
|
||||||
❌ **Kritische Defekte:**
|
|
||||||
- CSS-Variants existieren nicht → keine responsive Funktionalität
|
|
||||||
- Kein Logo
|
|
||||||
- Kein Toggle-Icon-Swap
|
|
||||||
- Submenu-Handling defekt
|
|
||||||
|
|
||||||
### 19.3 Nächste Schritte
|
|
||||||
|
|
||||||
1. **CSS-Variants entfernen** (alle `is-drawer-*` Klassen)
|
|
||||||
2. **Standard DaisyUI-Pattern verwenden** (ohne Custom-Variants)
|
|
||||||
3. **Logo hinzufügen** (persistent, size-8)
|
|
||||||
4. **Simplify:** Mobile = Overlay, Desktop = Persistent (keine collapsed State)
|
|
||||||
5. **Tests schreiben** (Component + Accessibility)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 20. Anhang
|
|
||||||
|
|
||||||
### 20.1 Verwendete CSS-Klassen (alphabetisch)
|
|
||||||
|
|
||||||
```
|
|
||||||
avatar, avatar-placeholder, bg-base-100, bg-base-200, bg-neutral,
|
|
||||||
btn, btn-circle, btn-ghost, btn-square, cursor-pointer, drawer,
|
|
||||||
drawer-content, drawer-overlay, drawer-side, drawer-toggle, dropdown,
|
|
||||||
dropdown-content, dropdown-end, dropdown-top, flex, flex-col,
|
|
||||||
focus:outline-none, focus:ring-2, focus:ring-primary,
|
|
||||||
focus-within:outline-none, focus-within:ring-2, gap-2, gap-4,
|
|
||||||
is-drawer-close:*, is-drawer-open:*, items-center, items-start,
|
|
||||||
mb-2, menu, menu-sm, menu-title, min-h-full, mr-2, mt-3, mt-auto,
|
|
||||||
p-2, p-4, rounded-box, rounded-full, select, select-sm, shadow,
|
|
||||||
shadow-sm, size-4, size-5, sr-only, text-lg, text-neutral-content,
|
|
||||||
text-sm, theme-controller, toggle, tooltip, tooltip-right, w-12,
|
|
||||||
w-52, w-64, w-full, z-1
|
|
||||||
```
|
|
||||||
|
|
||||||
### 20.2 Verwendete ARIA-Attribute
|
|
||||||
|
|
||||||
```
|
|
||||||
aria-busy, aria-controls, aria-describedby, aria-expanded,
|
|
||||||
aria-haspopup, aria-hidden, aria-label, aria-labelledby,
|
|
||||||
aria-live, role="alert", role="button", role="menu",
|
|
||||||
role="menubar", role="menuitem", role="none", role="status"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 20.3 Relevante Links
|
|
||||||
|
|
||||||
- [DaisyUI Drawer Docs](https://daisyui.com/components/drawer/)
|
|
||||||
- [Tailwind CSS Custom Variants](https://tailwindcss.com/docs/adding-custom-styles#adding-custom-variants)
|
|
||||||
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
|
|
||||||
- [Phoenix LiveView Docs](https://hexdocs.pm/phoenix_live_view/)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Ende des Berichts**
|
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,233 +0,0 @@
|
||||||
# Analyse der fehlschlagenden Tests
|
|
||||||
|
|
||||||
## Übersicht
|
|
||||||
|
|
||||||
**Gesamtanzahl fehlschlagender Tests:** 5
|
|
||||||
- **show_test.exs:** 1 Fehler
|
|
||||||
- **sidebar_test.exs:** 4 Fehler
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Kategorisierung
|
|
||||||
|
|
||||||
### Kategorie 1: Test-Assertions passen nicht zur Implementierung (4 Tests)
|
|
||||||
|
|
||||||
Diese Tests erwarten bestimmte Werte/Attribute, die in der aktuellen Implementierung anders sind oder fehlen.
|
|
||||||
|
|
||||||
### Kategorie 2: Datenbank-Isolation Problem (1 Test)
|
|
||||||
|
|
||||||
Ein Test schlägt fehl, weil die Datenbank nicht korrekt isoliert ist.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Detaillierte Analyse
|
|
||||||
|
|
||||||
### 1. `show_test.exs` - Custom Fields Sichtbarkeit
|
|
||||||
|
|
||||||
**Test:** `does not display Custom Fields section when no custom fields exist` (Zeile 112)
|
|
||||||
|
|
||||||
**Problem:**
|
|
||||||
- Der Test erwartet, dass die "Custom Fields" Sektion NICHT angezeigt wird, wenn keine Custom Fields existieren
|
|
||||||
- Die Sektion wird aber angezeigt, weil in der Datenbank noch Custom Fields von anderen Tests vorhanden sind
|
|
||||||
|
|
||||||
**Ursache:**
|
|
||||||
- Die LiveView lädt alle Custom Fields aus der Datenbank (Zeile 238-242 in `show.ex`)
|
|
||||||
- Die Test-Datenbank wird nicht zwischen Tests geleert
|
|
||||||
- Da `async: false` verwendet wird, sollten die Tests sequenziell laufen, aber Custom Fields bleiben in der Datenbank
|
|
||||||
|
|
||||||
**Kategorie:** Datenbank-Isolation Problem
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. `sidebar_test.exs` - Settings Link
|
|
||||||
|
|
||||||
**Test:** `T3.1: renders flat menu items with icons and labels` (Zeile 174)
|
|
||||||
|
|
||||||
**Problem:**
|
|
||||||
- Test erwartet `href="#"` für Settings
|
|
||||||
- Tatsächlicher Wert: `href="/settings"`
|
|
||||||
|
|
||||||
**Ursache:**
|
|
||||||
- Die Implementierung verwendet einen echten Link `~p"/settings"` (Zeile 100 in `sidebar.ex`)
|
|
||||||
- Der Test erwartet einen Placeholder-Link `href="#"`
|
|
||||||
|
|
||||||
**Kategorie:** Test-Assertion passt nicht zur Implementierung
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. `sidebar_test.exs` - Drawer Overlay CSS-Klasse
|
|
||||||
|
|
||||||
**Test:** `drawer overlay is present` (Zeile 747)
|
|
||||||
|
|
||||||
**Problem:**
|
|
||||||
- Test sucht nach exakt `class="drawer-overlay"`
|
|
||||||
- Tatsächlicher Wert: `class="drawer-overlay lg:hidden focus:outline-none focus:ring-2 focus:ring-primary"`
|
|
||||||
|
|
||||||
**Ursache:**
|
|
||||||
- Der Test verwendet eine exakte String-Suche (`~s(class="drawer-overlay")`)
|
|
||||||
- Die Implementierung hat mehrere CSS-Klassen
|
|
||||||
|
|
||||||
**Kategorie:** Test-Assertion passt nicht zur Implementierung
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. `sidebar_test.exs` - Toggle Button ARIA-Attribut
|
|
||||||
|
|
||||||
**Test:** `T5.2: toggle button has correct ARIA attributes` (Zeile 324)
|
|
||||||
|
|
||||||
**Problem:**
|
|
||||||
- Test erwartet `aria-controls="main-sidebar"` am Toggle-Button
|
|
||||||
- Das Attribut fehlt in der Implementierung (Zeile 45-65 in `sidebar.ex`)
|
|
||||||
|
|
||||||
**Ursache:**
|
|
||||||
- Das `aria-controls` Attribut wurde nicht in der Implementierung hinzugefügt
|
|
||||||
- Der Test erwartet es für bessere Accessibility
|
|
||||||
|
|
||||||
**Kategorie:** Test-Assertion passt nicht zur Implementierung (Accessibility-Feature fehlt)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. `sidebar_test.exs` - Contribution Settings Link
|
|
||||||
|
|
||||||
**Test:** `sidebar structure is complete with all sections` (Zeile 501)
|
|
||||||
|
|
||||||
**Problem:**
|
|
||||||
- Test erwartet Link `/contribution_settings`
|
|
||||||
- Tatsächlicher Link: `/membership_fee_settings`
|
|
||||||
|
|
||||||
**Ursache:**
|
|
||||||
- Der Test hat eine veraltete/inkorrekte Erwartung
|
|
||||||
- Die Implementierung verwendet `/membership_fee_settings` (Zeile 96 in `sidebar.ex`)
|
|
||||||
|
|
||||||
**Kategorie:** Test-Assertion passt nicht zur Implementierung (veralteter Test)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Lösungsvorschläge
|
|
||||||
|
|
||||||
### Lösung 1: `show_test.exs` - Custom Fields Sichtbarkeit
|
|
||||||
|
|
||||||
**Option A: Test-Datenbank bereinigen (Empfohlen)**
|
|
||||||
- Im `setup` Block alle Custom Fields löschen, bevor der Test läuft
|
|
||||||
- Oder: Explizit prüfen, dass keine Custom Fields existieren
|
|
||||||
|
|
||||||
**Option B: Test anpassen**
|
|
||||||
- Den Test so anpassen, dass er explizit alle Custom Fields löscht
|
|
||||||
- Oder: Die LiveView-Logik ändern, um nur Custom Fields zu laden, die tatsächlich existieren
|
|
||||||
|
|
||||||
**Empfehlung:** Option A - Im Test-Setup alle Custom Fields löschen
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
setup do
|
|
||||||
# Clean up any existing custom fields
|
|
||||||
Mv.Membership.CustomField
|
|
||||||
|> Ash.read!()
|
|
||||||
|> Enum.each(&Ash.destroy!/1)
|
|
||||||
|
|
||||||
# Create test member
|
|
||||||
{:ok, member} = ...
|
|
||||||
%{member: member}
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Lösung 2: `sidebar_test.exs` - Settings Link
|
|
||||||
|
|
||||||
**Option A: Test anpassen (Empfohlen)**
|
|
||||||
- Test ändern, um `href="/settings"` zu erwarten statt `href="#"`
|
|
||||||
|
|
||||||
**Option B: Implementierung ändern**
|
|
||||||
- Settings-Link zu `href="#"` ändern (nicht empfohlen, da es ein echter Link sein sollte)
|
|
||||||
|
|
||||||
**Empfehlung:** Option A - Test anpassen
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
# Zeile 190 ändern von:
|
|
||||||
assert html =~ ~s(href="#")
|
|
||||||
# zu:
|
|
||||||
assert html =~ ~s(href="/settings")
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Lösung 3: `sidebar_test.exs` - Drawer Overlay CSS-Klasse
|
|
||||||
|
|
||||||
**Option A: Test anpassen (Empfohlen)**
|
|
||||||
- Test ändern, um nach der Klasse in der Klasse-Liste zu suchen (mit `has_class?` Helper)
|
|
||||||
|
|
||||||
**Option B: Regex verwenden**
|
|
||||||
- Regex verwenden, um die Klasse zu finden
|
|
||||||
|
|
||||||
**Empfehlung:** Option A - Test anpassen
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
# Zeile 752 ändern von:
|
|
||||||
assert html =~ ~s(class="drawer-overlay")
|
|
||||||
# zu:
|
|
||||||
assert has_class?(html, "drawer-overlay")
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Lösung 4: `sidebar_test.exs` - Toggle Button ARIA-Attribut
|
|
||||||
|
|
||||||
**Option A: Implementierung anpassen (Empfohlen)**
|
|
||||||
- `aria-controls="main-sidebar"` zum Toggle-Button hinzufügen
|
|
||||||
|
|
||||||
**Option B: Test anpassen**
|
|
||||||
- Test entfernen oder als optional markieren (nicht empfohlen für Accessibility)
|
|
||||||
|
|
||||||
**Empfehlung:** Option A - Implementierung anpassen
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
# In sidebar.ex Zeile 45-52, aria-controls hinzufügen:
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="sidebar-toggle"
|
|
||||||
class="hidden lg:flex ml-auto btn btn-ghost btn-sm btn-square"
|
|
||||||
aria-label={gettext("Toggle sidebar")}
|
|
||||||
aria-controls="main-sidebar"
|
|
||||||
aria-expanded="true"
|
|
||||||
onclick="toggleSidebar()"
|
|
||||||
>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Lösung 5: `sidebar_test.exs` - Contribution Settings Link
|
|
||||||
|
|
||||||
**Option A: Test anpassen (Empfohlen)**
|
|
||||||
- Test ändern, um `/membership_fee_settings` statt `/contribution_settings` zu erwarten
|
|
||||||
|
|
||||||
**Option B: Link hinzufügen**
|
|
||||||
- Einen neuen Link `/contribution_settings` hinzufügen (nicht empfohlen, da redundant)
|
|
||||||
|
|
||||||
**Empfehlung:** Option A - Test anpassen
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
# Zeile 519 ändern von:
|
|
||||||
"/contribution_settings",
|
|
||||||
# zu:
|
|
||||||
# Entfernen oder durch "/membership_fee_settings" ersetzen
|
|
||||||
# (da "/membership_fee_settings" bereits in Zeile 518 vorhanden ist)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Zusammenfassung der empfohlenen Änderungen
|
|
||||||
|
|
||||||
1. **show_test.exs:** Custom Fields im Setup löschen
|
|
||||||
2. **sidebar_test.exs (T3.1):** Settings-Link Assertion anpassen
|
|
||||||
3. **sidebar_test.exs (drawer overlay):** CSS-Klasse-Suche mit Helper-Funktion
|
|
||||||
4. **sidebar_test.exs (T5.2):** `aria-controls` Attribut zur Implementierung hinzufügen
|
|
||||||
5. **sidebar_test.exs (edge cases):** Falschen Link aus erwarteter Liste entfernen
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Priorisierung
|
|
||||||
|
|
||||||
1. **Hoch:** Lösung 1 (show_test.exs) - Datenbank-Isolation ist wichtig
|
|
||||||
2. **Mittel:** Lösung 4 (ARIA-Attribut) - Accessibility-Verbesserung
|
|
||||||
3. **Niedrig:** Lösungen 2, 3, 5 - Einfache Test-Anpassungen
|
|
||||||
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
# Test Status: Membership Fee UI Components
|
|
||||||
|
|
||||||
**Date:** 2025-01-XX
|
|
||||||
**Status:** Tests Written - Implementation Complete
|
|
||||||
|
|
||||||
## Übersicht
|
|
||||||
|
|
||||||
Alle Tests für die Membership Fee UI-Komponenten wurden geschrieben. Die Tests sind TDD-konform geschrieben und sollten erfolgreich laufen, da die Implementation bereits vorhanden ist.
|
|
||||||
|
|
||||||
## Test-Dateien
|
|
||||||
|
|
||||||
### Helper Module Tests
|
|
||||||
|
|
||||||
**Datei:** `test/mv_web/helpers/membership_fee_helpers_test.exs`
|
|
||||||
- ✅ format_currency/1 formats correctly
|
|
||||||
- ✅ format_interval/1 formats all interval types
|
|
||||||
- ✅ format_cycle_range/2 formats date ranges correctly
|
|
||||||
- ✅ get_last_completed_cycle/2 returns correct cycle
|
|
||||||
- ✅ get_current_cycle/2 returns correct cycle
|
|
||||||
- ✅ status_color/1 returns correct color classes
|
|
||||||
- ✅ status_icon/1 returns correct icon names
|
|
||||||
|
|
||||||
**Status:** Alle Tests sollten erfolgreich sein (Implementation vorhanden)
|
|
||||||
|
|
||||||
**Datei:** `test/mv_web/member_live/index/membership_fee_status_test.exs`
|
|
||||||
- ✅ load_cycles_for_members/2 efficiently loads cycles
|
|
||||||
- ✅ get_cycle_status_for_member/2 returns correct status
|
|
||||||
- ✅ format_cycle_status_badge/1 returns correct badge
|
|
||||||
|
|
||||||
**Status:** Alle Tests sollten erfolgreich sein (Implementation vorhanden)
|
|
||||||
|
|
||||||
### Member List View Tests
|
|
||||||
|
|
||||||
**Datei:** `test/mv_web/member_live/index_membership_fee_status_test.exs`
|
|
||||||
- ✅ Status column displays correctly
|
|
||||||
- ✅ Shows last completed cycle status by default
|
|
||||||
- ✅ Toggle switches to current cycle view
|
|
||||||
- ✅ Color coding for paid/unpaid/suspended
|
|
||||||
- ✅ Filter "Unpaid in last cycle" works
|
|
||||||
- ✅ Filter "Unpaid in current cycle" works
|
|
||||||
- ✅ Handles members without cycles gracefully
|
|
||||||
- ✅ Loads cycles efficiently without N+1 queries
|
|
||||||
|
|
||||||
**Status:** Alle Tests sollten erfolgreich sein (Implementation vorhanden)
|
|
||||||
|
|
||||||
### Member Detail View Tests
|
|
||||||
|
|
||||||
**Datei:** `test/mv_web/member_live/show_membership_fees_test.exs`
|
|
||||||
- ✅ Cycles table displays all cycles
|
|
||||||
- ✅ Table columns show correct data
|
|
||||||
- ✅ Membership fee type dropdown shows only same-interval types
|
|
||||||
- ✅ Warning displayed if different interval selected
|
|
||||||
- ✅ Status change actions work (mark as paid/suspended/unpaid)
|
|
||||||
- ✅ Cycle regeneration works
|
|
||||||
- ✅ Handles members without membership fee type gracefully
|
|
||||||
|
|
||||||
**Status:** Alle Tests sollten erfolgreich sein (Implementation vorhanden)
|
|
||||||
|
|
||||||
### Membership Fee Types Admin Tests
|
|
||||||
|
|
||||||
**Datei:** `test/mv_web/live/membership_fee_type_live/index_test.exs`
|
|
||||||
- ✅ List displays all types with correct data
|
|
||||||
- ✅ Member count column shows correct count
|
|
||||||
- ✅ Create button navigates to form
|
|
||||||
- ✅ Edit button per row navigates to edit form
|
|
||||||
- ✅ Delete button disabled if type is in use
|
|
||||||
- ✅ Delete button works if type is not in use
|
|
||||||
- ✅ Only admin can access
|
|
||||||
|
|
||||||
**Status:** Alle Tests sollten erfolgreich sein (Implementation vorhanden)
|
|
||||||
|
|
||||||
**Datei:** `test/mv_web/live/membership_fee_type_live/form_test.exs`
|
|
||||||
- ✅ Create form works
|
|
||||||
- ✅ Edit form loads existing type data
|
|
||||||
- ✅ Interval field editable on create
|
|
||||||
- ✅ Interval field grayed out on edit
|
|
||||||
- ✅ Amount change warning displays on edit
|
|
||||||
- ✅ Amount change warning shows correct affected member count
|
|
||||||
- ✅ Amount change can be confirmed
|
|
||||||
- ✅ Amount change can be cancelled
|
|
||||||
- ✅ Validation errors display correctly
|
|
||||||
- ✅ Only admin can access
|
|
||||||
|
|
||||||
**Status:** Alle Tests sollten erfolgreich sein (Implementation vorhanden)
|
|
||||||
|
|
||||||
### Member Form Tests
|
|
||||||
|
|
||||||
**Datei:** `test/mv_web/member_live/form_membership_fee_type_test.exs`
|
|
||||||
- ✅ Membership fee type dropdown displays in form
|
|
||||||
- ✅ Shows available types
|
|
||||||
- ✅ Filters to same interval types if member has type
|
|
||||||
- ✅ Warning displayed if different interval selected
|
|
||||||
- ✅ Warning cleared if same interval selected
|
|
||||||
- ✅ Form saves with selected membership fee type
|
|
||||||
- ✅ New members get default membership fee type
|
|
||||||
|
|
||||||
**Status:** Alle Tests sollten erfolgreich sein (Implementation vorhanden)
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
|
|
||||||
**Datei:** `test/mv_web/member_live/membership_fee_integration_test.exs`
|
|
||||||
- ✅ End-to-end: Create type → Assign to member → View cycles → Change status
|
|
||||||
- ✅ End-to-end: Change member type → Cycles regenerate
|
|
||||||
- ✅ End-to-end: Update settings → New members get default type
|
|
||||||
- ✅ End-to-end: Delete cycle → Confirmation → Cycle deleted
|
|
||||||
- ✅ End-to-end: Edit cycle amount → Modal → Amount updated
|
|
||||||
|
|
||||||
**Status:** Alle Tests sollten erfolgreich sein (Implementation vorhanden)
|
|
||||||
|
|
||||||
## Test-Ausführung
|
|
||||||
|
|
||||||
Alle Tests können mit folgenden Befehlen ausgeführt werden:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Alle Tests
|
|
||||||
mix test
|
|
||||||
|
|
||||||
# Nur Membership Fee Tests
|
|
||||||
mix test test/mv_web/helpers/membership_fee_helpers_test.exs
|
|
||||||
mix test test/mv_web/member_live/
|
|
||||||
mix test test/mv_web/live/membership_fee_type_live/
|
|
||||||
|
|
||||||
# Mit Coverage
|
|
||||||
mix test --cover
|
|
||||||
```
|
|
||||||
|
|
||||||
## Bekannte Probleme
|
|
||||||
|
|
||||||
Keine bekannten Probleme. Alle Tests sollten erfolgreich laufen, da die Implementation bereits vorhanden ist.
|
|
||||||
|
|
||||||
## Nächste Schritte
|
|
||||||
|
|
||||||
1. ✅ Tests geschrieben
|
|
||||||
2. ⏳ Tests ausführen und verifizieren
|
|
||||||
3. ⏳ Eventuelle Anpassungen basierend auf Test-Ergebnissen
|
|
||||||
4. ⏳ Code-Review durchführen
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,15 @@
|
||||||
defmodule Mv.Accounts do
|
defmodule Mv.Accounts do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
AshAuthentication specific domain to handle Authentication for users.
|
AshAuthentication specific domain to handle Authentication for users.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
- `User` - User accounts with authentication methods (password, OIDC)
|
||||||
|
- `Token` - Session tokens for authentication
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
The domain exposes these main actions:
|
||||||
|
- User CRUD: `create_user/1`, `list_users/0`, `update_user/2`, `destroy_user/1`
|
||||||
|
- Authentication: `create_register_with_rauthy/1`, `read_sign_in_with_rauthy/1`
|
||||||
"""
|
"""
|
||||||
use Ash.Domain,
|
use Ash.Domain,
|
||||||
extensions: [AshAdmin.Domain, AshPhoenix]
|
extensions: [AshAdmin.Domain, AshPhoenix]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
defmodule Mv.Accounts.Token do
|
defmodule Mv.Accounts.Token do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
AshAuthentication specific ressource
|
AshAuthentication Token Resource for session management.
|
||||||
|
|
||||||
|
This resource is used by AshAuthentication to manage authentication tokens
|
||||||
|
for user sessions. Tokens are automatically created and managed by the
|
||||||
|
authentication system.
|
||||||
"""
|
"""
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer,
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ defmodule Mv.Membership do
|
||||||
The domain exposes these main actions:
|
The domain exposes these main actions:
|
||||||
- Member CRUD: `create_member/1`, `list_members/0`, `update_member/2`, `destroy_member/1`
|
- Member CRUD: `create_member/1`, `list_members/0`, `update_member/2`, `destroy_member/1`
|
||||||
- Custom field value management: `create_custom_field_value/1`, `list_custom_field_values/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.
|
- Custom field management: `create_custom_field/1`, `list_custom_fields/0`, `list_required_custom_fields/0`, etc.
|
||||||
- Settings management: `get_settings/0`, `update_settings/2`
|
- Settings management: `get_settings/0`, `update_settings/2`, `update_member_field_visibility/2`, `update_single_member_field_visibility/3`
|
||||||
|
|
||||||
## Admin Interface
|
## Admin Interface
|
||||||
The domain is configured with AshAdmin for management UI.
|
The domain is configured with AshAdmin for management UI.
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,13 @@ defmodule Mv.MembershipFees do
|
||||||
- `MembershipFeeType` - Defines membership fee types with intervals and amounts
|
- `MembershipFeeType` - Defines membership fee types with intervals and amounts
|
||||||
- `MembershipFeeCycle` - Individual membership fee cycles per member
|
- `MembershipFeeCycle` - Individual membership fee cycles per member
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
The domain exposes these main actions:
|
||||||
|
- MembershipFeeType CRUD: `create_membership_fee_type/1`, `list_membership_fee_types/0`, `update_membership_fee_type/2`, `destroy_membership_fee_type/1`
|
||||||
|
- MembershipFeeCycle CRUD: `create_membership_fee_cycle/1`, `list_membership_fee_cycles/0`, `update_membership_fee_cycle/2`, `destroy_membership_fee_cycle/1`
|
||||||
|
|
||||||
|
Note: LiveViews may use direct Ash calls instead of these domain functions for performance or flexibility.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
This domain handles the complete membership fee lifecycle including:
|
This domain handles the complete membership fee lifecycle including:
|
||||||
- Fee type definitions (monthly, quarterly, half-yearly, yearly)
|
- Fee type definitions (monthly, quarterly, half-yearly, yearly)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ defmodule Mv.Authorization do
|
||||||
|
|
||||||
## Public API
|
## Public API
|
||||||
The domain exposes these main actions:
|
The domain exposes these main actions:
|
||||||
- Role CRUD: `create_role/1`, `list_roles/0`, `update_role/2`, `destroy_role/1`
|
- Role CRUD: `create_role/1`, `list_roles/0`, `get_role/1`, `update_role/2`, `destroy_role/1`
|
||||||
|
|
||||||
## Admin Interface
|
## Admin Interface
|
||||||
The domain is configured with AshAdmin for management UI.
|
The domain is configured with AshAdmin for management UI.
|
||||||
|
|
|
||||||
|
|
@ -302,28 +302,15 @@ defmodule Mv.Membership.Import.MemberCSV do
|
||||||
max_errors = Keyword.get(opts, :max_errors, 50)
|
max_errors = Keyword.get(opts, :max_errors, 50)
|
||||||
|
|
||||||
{inserted, failed, errors, _collected_error_count, truncated?} =
|
{inserted, failed, errors, _collected_error_count, truncated?} =
|
||||||
Enum.reduce(chunk_rows_with_lines, {0, 0, [], 0, false}, fn {line_number, row_map},
|
Enum.reduce(chunk_rows_with_lines, {0, 0, [], 0, false}, fn {line_number, row_map}, acc ->
|
||||||
{acc_inserted, acc_failed,
|
current_error_count = existing_error_count + elem(acc, 3)
|
||||||
acc_errors, acc_error_count,
|
|
||||||
acc_truncated?} ->
|
|
||||||
current_error_count = existing_error_count + acc_error_count
|
|
||||||
|
|
||||||
case process_row(row_map, line_number, custom_field_lookup) do
|
case process_row(row_map, line_number, custom_field_lookup) do
|
||||||
{:ok, _member} ->
|
{:ok, _member} ->
|
||||||
{acc_inserted + 1, acc_failed, acc_errors, acc_error_count, acc_truncated?}
|
update_inserted(acc)
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
new_acc_failed = acc_failed + 1
|
handle_row_error(acc, error, current_error_count, max_errors)
|
||||||
|
|
||||||
# Only collect errors if under limit
|
|
||||||
{new_acc_errors, new_error_count, new_truncated?} =
|
|
||||||
if current_error_count < max_errors do
|
|
||||||
{[error | acc_errors], acc_error_count + 1, acc_truncated?}
|
|
||||||
else
|
|
||||||
{acc_errors, acc_error_count, true}
|
|
||||||
end
|
|
||||||
|
|
||||||
{acc_inserted, new_acc_failed, new_acc_errors, new_error_count, new_truncated?}
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -397,11 +384,9 @@ defmodule Mv.Membership.Import.MemberCSV do
|
||||||
|
|
||||||
# Extracts the first error from a changeset and converts it to a MemberCSV.Error struct
|
# Extracts the first error from a changeset and converts it to a MemberCSV.Error struct
|
||||||
defp extract_changeset_error(changeset, csv_line_number) do
|
defp extract_changeset_error(changeset, csv_line_number) do
|
||||||
case Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
errors = Ecto.Changeset.traverse_errors(changeset, &format_error_message/1)
|
||||||
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
|
||||||
String.replace(acc, "%{#{key}}", to_string(value))
|
case errors do
|
||||||
end)
|
|
||||||
end) do
|
|
||||||
%{email: [message | _]} ->
|
%{email: [message | _]} ->
|
||||||
# Email-specific error
|
# Email-specific error
|
||||||
%Error{
|
%Error{
|
||||||
|
|
@ -430,6 +415,56 @@ defmodule Mv.Membership.Import.MemberCSV do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Helper function to update accumulator when row is successfully inserted
|
||||||
|
defp update_inserted({acc_inserted, acc_failed, acc_errors, acc_error_count, acc_truncated?}) do
|
||||||
|
{acc_inserted + 1, acc_failed, acc_errors, acc_error_count, acc_truncated?}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper function to handle row error with error count limit checking
|
||||||
|
defp handle_row_error(
|
||||||
|
{acc_inserted, acc_failed, acc_errors, acc_error_count, acc_truncated?},
|
||||||
|
error,
|
||||||
|
current_error_count,
|
||||||
|
max_errors
|
||||||
|
) do
|
||||||
|
new_acc_failed = acc_failed + 1
|
||||||
|
|
||||||
|
{new_acc_errors, new_error_count, new_truncated?} =
|
||||||
|
collect_error_if_under_limit(
|
||||||
|
error,
|
||||||
|
acc_errors,
|
||||||
|
acc_error_count,
|
||||||
|
acc_truncated?,
|
||||||
|
current_error_count,
|
||||||
|
max_errors
|
||||||
|
)
|
||||||
|
|
||||||
|
{acc_inserted, new_acc_failed, new_acc_errors, new_error_count, new_truncated?}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper function to collect error only if under limit
|
||||||
|
defp collect_error_if_under_limit(
|
||||||
|
error,
|
||||||
|
acc_errors,
|
||||||
|
acc_error_count,
|
||||||
|
acc_truncated?,
|
||||||
|
current_error_count,
|
||||||
|
max_errors
|
||||||
|
) do
|
||||||
|
if current_error_count < max_errors do
|
||||||
|
{[error | acc_errors], acc_error_count + 1, acc_truncated?}
|
||||||
|
else
|
||||||
|
{acc_errors, acc_error_count, true}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Formats error message by replacing placeholders
|
||||||
|
defp format_error_message({msg, opts}) do
|
||||||
|
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
||||||
|
String.replace(acc, "%{#{key}}", to_string(value))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
# Maps changeset error messages to appropriate Gettext messages
|
# Maps changeset error messages to appropriate Gettext messages
|
||||||
defp gettext_error_message(message) when is_binary(message) do
|
defp gettext_error_message(message) when is_binary(message) do
|
||||||
cond do
|
cond do
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
defmodule MvWeb.PageController do
|
defmodule MvWeb.PageController do
|
||||||
|
@moduledoc """
|
||||||
|
Controller for rendering the homepage.
|
||||||
|
|
||||||
|
This controller handles the root route and renders the application's
|
||||||
|
homepage view.
|
||||||
|
"""
|
||||||
use MvWeb, :controller
|
use MvWeb, :controller
|
||||||
|
|
||||||
def home(conn, _params) do
|
def home(conn, _params) do
|
||||||
|
|
|
||||||
|
|
@ -1,345 +0,0 @@
|
||||||
defmodule MvWeb.ContributionPeriodLive.Show do
|
|
||||||
@moduledoc """
|
|
||||||
Mock-up LiveView for Member Contribution Periods (Admin/Treasurer View).
|
|
||||||
|
|
||||||
This is a preview-only page that displays the planned UI for viewing
|
|
||||||
and managing contribution periods for a specific member.
|
|
||||||
It shows static mock data and is not functional.
|
|
||||||
|
|
||||||
## Planned Features (Future Implementation)
|
|
||||||
- Display all contribution periods for a member
|
|
||||||
- Show period dates, interval, amount, and status
|
|
||||||
- Quick status change (paid/unpaid/suspended)
|
|
||||||
- Bulk marking of multiple periods
|
|
||||||
- Notes per period
|
|
||||||
|
|
||||||
## Note
|
|
||||||
This page is intentionally non-functional and serves as a UI mockup
|
|
||||||
for the upcoming Membership Contributions feature.
|
|
||||||
"""
|
|
||||||
use MvWeb, :live_view
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def mount(_params, _session, socket) do
|
|
||||||
{:ok,
|
|
||||||
socket
|
|
||||||
|> assign(:page_title, gettext("Member Contributions"))
|
|
||||||
|> assign(:member, mock_member())
|
|
||||||
|> assign(:periods, mock_periods())
|
|
||||||
|> assign(:selected_periods, MapSet.new())}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
|
||||||
<.mockup_warning />
|
|
||||||
|
|
||||||
<.header>
|
|
||||||
{gettext("Contributions for %{name}", name: MvWeb.Helpers.MemberHelpers.display_name(@member))}
|
|
||||||
<:subtitle>
|
|
||||||
{gettext("Contribution type")}:
|
|
||||||
<span class="font-semibold">{@member.contribution_type}</span>
|
|
||||||
· {gettext("Member since")}: <span class="font-mono">{@member.joined_at}</span>
|
|
||||||
</:subtitle>
|
|
||||||
<:actions>
|
|
||||||
<.link navigate={~p"/membership_fee_settings"} class="btn btn-ghost btn-sm">
|
|
||||||
<.icon name="hero-arrow-left" class="size-4" />
|
|
||||||
{gettext("Back to Settings")}
|
|
||||||
</.link>
|
|
||||||
</:actions>
|
|
||||||
</.header>
|
|
||||||
|
|
||||||
<%!-- Member Info Card --%>
|
|
||||||
<div class="mb-6 shadow card bg-base-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
|
|
||||||
<div>
|
|
||||||
<span class="text-sm text-base-content/60">{gettext("Email")}</span>
|
|
||||||
<p class="font-medium">{@member.email}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-sm text-base-content/60">{gettext("Contribution Start")}</span>
|
|
||||||
<p class="font-mono">{@member.contribution_start}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-sm text-base-content/60">{gettext("Total Contributions")}</span>
|
|
||||||
<p class="font-semibold">{length(@periods)}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-sm text-base-content/60">{gettext("Open Contributions")}</span>
|
|
||||||
<p class="font-semibold text-error">
|
|
||||||
{Enum.count(@periods, &(&1.status == :unpaid))}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%!-- Contribution Type Change --%>
|
|
||||||
<div class="mb-6 card bg-base-200">
|
|
||||||
<div class="py-4 card-body">
|
|
||||||
<div class="flex flex-wrap items-center gap-4">
|
|
||||||
<span class="font-semibold">{gettext("Change Contribution Type")}:</span>
|
|
||||||
<select class="w-64 select select-bordered select-sm" disabled>
|
|
||||||
<option selected>{@member.contribution_type} (60,00 €, {gettext("Yearly")})</option>
|
|
||||||
<option>{gettext("Reduced")} (30,00 €, {gettext("Yearly")})</option>
|
|
||||||
<option>{gettext("Honorary")} (0,00 €, {gettext("Yearly")})</option>
|
|
||||||
</select>
|
|
||||||
<span
|
|
||||||
class="text-sm text-base-content/60 cursor-help tooltip tooltip-bottom"
|
|
||||||
data-tip={
|
|
||||||
gettext(
|
|
||||||
"Members can only switch between contribution types with the same payment interval (e.g., yearly to yearly). This prevents complex period overlaps."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<.icon name="hero-question-mark-circle" class="inline size-4" />
|
|
||||||
{gettext("Why are not all contribution types shown?")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%!-- Bulk Actions --%>
|
|
||||||
<div class="flex flex-wrap items-center gap-4 mb-4">
|
|
||||||
<span class="text-sm text-base-content/60">
|
|
||||||
{ngettext(
|
|
||||||
"%{count} period selected",
|
|
||||||
"%{count} periods selected",
|
|
||||||
MapSet.size(@selected_periods),
|
|
||||||
count: MapSet.size(@selected_periods)
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<button class="btn btn-sm btn-success" disabled>
|
|
||||||
<.icon name="hero-check" class="size-4" />
|
|
||||||
{gettext("Mark as Paid")}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-ghost" disabled>
|
|
||||||
<.icon name="hero-minus-circle" class="size-4" />
|
|
||||||
{gettext("Mark as Suspended")}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-ghost" disabled>
|
|
||||||
<.icon name="hero-x-circle" class="size-4" />
|
|
||||||
{gettext("Mark as Unpaid")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%!-- Periods Table --%>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="table table-zebra">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
<input type="checkbox" class="checkbox checkbox-sm" disabled />
|
|
||||||
</th>
|
|
||||||
<th>{gettext("Time Period")}</th>
|
|
||||||
<th>{gettext("Interval")}</th>
|
|
||||||
<th>{gettext("Amount")}</th>
|
|
||||||
<th>{gettext("Status")}</th>
|
|
||||||
<th>{gettext("Notes")}</th>
|
|
||||||
<th>{gettext("Actions")}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr :for={period <- @periods} class={period_row_class(period.status)}>
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="checkbox checkbox-sm"
|
|
||||||
checked={MapSet.member?(@selected_periods, period.id)}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="font-mono">
|
|
||||||
{period.period_start} – {period.period_end}
|
|
||||||
</div>
|
|
||||||
<div :if={period.is_current} class="mt-1 badge badge-info badge-sm">
|
|
||||||
{gettext("Current")}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge badge-outline badge-sm">{format_interval(period.interval)}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="font-mono">{format_currency(period.amount)}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<.status_badge status={period.status} />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span :if={period.notes} class="text-sm italic text-base-content/60">
|
|
||||||
{period.notes}
|
|
||||||
</span>
|
|
||||||
<span :if={!period.notes} class="text-base-content/30">—</span>
|
|
||||||
</td>
|
|
||||||
<td class="w-0 font-semibold whitespace-nowrap">
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<.link
|
|
||||||
href="#"
|
|
||||||
class={[
|
|
||||||
"cursor-not-allowed",
|
|
||||||
if(period.status == :paid, do: "invisible", else: "opacity-50")
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{gettext("Paid")}
|
|
||||||
</.link>
|
|
||||||
<.link
|
|
||||||
href="#"
|
|
||||||
class={[
|
|
||||||
"cursor-not-allowed",
|
|
||||||
if(period.status == :suspended, do: "invisible", else: "opacity-50")
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{gettext("Suspend")}
|
|
||||||
</.link>
|
|
||||||
<.link
|
|
||||||
href="#"
|
|
||||||
class={[
|
|
||||||
"cursor-not-allowed",
|
|
||||||
if(period.status != :paid, do: "invisible", else: "opacity-50")
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{gettext("Reopen")}
|
|
||||||
</.link>
|
|
||||||
<.link href="#" class="opacity-50 cursor-not-allowed">
|
|
||||||
{gettext("Note")}
|
|
||||||
</.link>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</Layouts.app>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
# Mock-up warning banner component - subtle orange style
|
|
||||||
defp mockup_warning(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div class="flex items-center gap-3 px-4 py-3 mb-6 border rounded-lg border-warning text-warning bg-base-100">
|
|
||||||
<.icon name="hero-exclamation-triangle" class="size-5 shrink-0" />
|
|
||||||
<div>
|
|
||||||
<span class="font-semibold">{gettext("Preview Mockup")}</span>
|
|
||||||
<span class="ml-2 text-sm text-base-content/70">
|
|
||||||
– {gettext("This page is not functional and only displays the planned features.")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
# Status badge component
|
|
||||||
attr :status, :atom, required: true
|
|
||||||
|
|
||||||
defp status_badge(%{status: :paid} = assigns) do
|
|
||||||
~H"""
|
|
||||||
<span class="gap-1 badge badge-success">
|
|
||||||
<.icon name="hero-check-circle-mini" class="size-3" />
|
|
||||||
{gettext("Paid")}
|
|
||||||
</span>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
defp status_badge(%{status: :unpaid} = assigns) do
|
|
||||||
~H"""
|
|
||||||
<span class="gap-1 badge badge-error">
|
|
||||||
<.icon name="hero-x-circle-mini" class="size-3" />
|
|
||||||
{gettext("Unpaid")}
|
|
||||||
</span>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
defp status_badge(%{status: :suspended} = assigns) do
|
|
||||||
~H"""
|
|
||||||
<span class="gap-1 badge badge-neutral">
|
|
||||||
<.icon name="hero-pause-circle-mini" class="size-3" />
|
|
||||||
{gettext("Suspended")}
|
|
||||||
</span>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
defp period_row_class(:unpaid), do: "bg-error/5"
|
|
||||||
defp period_row_class(:suspended), do: "bg-base-200/50"
|
|
||||||
defp period_row_class(_), do: ""
|
|
||||||
|
|
||||||
# Mock member data
|
|
||||||
defp mock_member do
|
|
||||||
%{
|
|
||||||
id: "123",
|
|
||||||
first_name: "Maria",
|
|
||||||
last_name: "Weber",
|
|
||||||
email: "maria.weber@example.de",
|
|
||||||
contribution_type: gettext("Regular"),
|
|
||||||
joined_at: "15.03.2021",
|
|
||||||
contribution_start: "01.01.2021"
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Mock periods data
|
|
||||||
defp mock_periods do
|
|
||||||
[
|
|
||||||
%{
|
|
||||||
id: "p1",
|
|
||||||
period_start: "01.01.2025",
|
|
||||||
period_end: "31.12.2025",
|
|
||||||
interval: :yearly,
|
|
||||||
amount: Decimal.new("60.00"),
|
|
||||||
status: :unpaid,
|
|
||||||
notes: nil,
|
|
||||||
is_current: true
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
id: "p2",
|
|
||||||
period_start: "01.01.2024",
|
|
||||||
period_end: "31.12.2024",
|
|
||||||
interval: :yearly,
|
|
||||||
amount: Decimal.new("60.00"),
|
|
||||||
status: :paid,
|
|
||||||
notes: gettext("Paid via bank transfer"),
|
|
||||||
is_current: false
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
id: "p3",
|
|
||||||
period_start: "01.01.2023",
|
|
||||||
period_end: "31.12.2023",
|
|
||||||
interval: :yearly,
|
|
||||||
amount: Decimal.new("50.00"),
|
|
||||||
status: :paid,
|
|
||||||
notes: nil,
|
|
||||||
is_current: false
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
id: "p4",
|
|
||||||
period_start: "01.01.2022",
|
|
||||||
period_end: "31.12.2022",
|
|
||||||
interval: :yearly,
|
|
||||||
amount: Decimal.new("50.00"),
|
|
||||||
status: :paid,
|
|
||||||
notes: nil,
|
|
||||||
is_current: false
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
id: "p5",
|
|
||||||
period_start: "01.01.2021",
|
|
||||||
period_end: "31.12.2021",
|
|
||||||
interval: :yearly,
|
|
||||||
amount: Decimal.new("50.00"),
|
|
||||||
status: :suspended,
|
|
||||||
notes: gettext("Joining year - reduced to 0"),
|
|
||||||
is_current: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp format_currency(%Decimal{} = amount) do
|
|
||||||
"#{Decimal.to_string(amount)} €"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp format_interval(:monthly), do: gettext("Monthly")
|
|
||||||
defp format_interval(:quarterly), do: gettext("Quarterly")
|
|
||||||
defp format_interval(:half_yearly), do: gettext("Half-yearly")
|
|
||||||
defp format_interval(:yearly), do: gettext("Yearly")
|
|
||||||
end
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
defmodule MvWeb.ContributionTypeLive.Index do
|
|
||||||
@moduledoc """
|
|
||||||
Mock-up LiveView for Contribution Types Management (Admin).
|
|
||||||
|
|
||||||
This is a preview-only page that displays the planned UI for managing
|
|
||||||
contribution types. It shows static mock data and is not functional.
|
|
||||||
|
|
||||||
## Planned Features (Future Implementation)
|
|
||||||
- List all contribution types
|
|
||||||
- Display: Name, Amount, Interval, Member count
|
|
||||||
- Create new contribution types
|
|
||||||
- Edit existing contribution types (name, amount, description - NOT interval)
|
|
||||||
- Delete contribution types (if no members assigned)
|
|
||||||
|
|
||||||
## Note
|
|
||||||
This page is intentionally non-functional and serves as a UI mockup
|
|
||||||
for the upcoming Membership Contributions feature.
|
|
||||||
"""
|
|
||||||
use MvWeb, :live_view
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def mount(_params, _session, socket) do
|
|
||||||
{:ok,
|
|
||||||
socket
|
|
||||||
|> assign(:page_title, gettext("Contribution Types"))
|
|
||||||
|> assign(:contribution_types, mock_contribution_types())}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
|
||||||
<.mockup_warning />
|
|
||||||
|
|
||||||
<.header>
|
|
||||||
{gettext("Contribution Types")}
|
|
||||||
<:subtitle>
|
|
||||||
{gettext("Manage contribution types for membership fees.")}
|
|
||||||
</:subtitle>
|
|
||||||
<:actions>
|
|
||||||
<button class="btn btn-primary" disabled>
|
|
||||||
<.icon name="hero-plus" /> {gettext("New Contribution Type")}
|
|
||||||
</button>
|
|
||||||
</:actions>
|
|
||||||
</.header>
|
|
||||||
|
|
||||||
<.table id="contribution_types" rows={@contribution_types} row_id={fn ct -> "ct-#{ct.id}" end}>
|
|
||||||
<:col :let={ct} label={gettext("Name")}>
|
|
||||||
<span class="font-medium">{ct.name}</span>
|
|
||||||
<p :if={ct.description} class="text-sm text-base-content/60">{ct.description}</p>
|
|
||||||
</:col>
|
|
||||||
|
|
||||||
<:col :let={ct} label={gettext("Amount")}>
|
|
||||||
<span class="font-mono">{format_currency(ct.amount)}</span>
|
|
||||||
</:col>
|
|
||||||
|
|
||||||
<:col :let={ct} label={gettext("Interval")}>
|
|
||||||
<span class="badge badge-outline">{format_interval(ct.interval)}</span>
|
|
||||||
</:col>
|
|
||||||
|
|
||||||
<:col :let={ct} label={gettext("Members")}>
|
|
||||||
<span class="badge badge-ghost">{ct.member_count}</span>
|
|
||||||
</:col>
|
|
||||||
|
|
||||||
<:action :let={_ct}>
|
|
||||||
<button class="btn btn-ghost btn-xs" disabled title={gettext("Edit")}>
|
|
||||||
<.icon name="hero-pencil" class="size-4" />
|
|
||||||
</button>
|
|
||||||
</:action>
|
|
||||||
|
|
||||||
<:action :let={ct}>
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost btn-xs text-error"
|
|
||||||
disabled
|
|
||||||
title={
|
|
||||||
if ct.member_count > 0,
|
|
||||||
do: gettext("Cannot delete - members assigned"),
|
|
||||||
else: gettext("Delete")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<.icon name="hero-trash" class="size-4" />
|
|
||||||
</button>
|
|
||||||
</:action>
|
|
||||||
</.table>
|
|
||||||
|
|
||||||
<.info_card />
|
|
||||||
</Layouts.app>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
# Mock-up warning banner component - subtle orange style
|
|
||||||
defp mockup_warning(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div class="border border-warning text-warning bg-base-100 rounded-lg px-4 py-3 mb-6 flex items-center gap-3">
|
|
||||||
<.icon name="hero-exclamation-triangle" class="size-5 shrink-0" />
|
|
||||||
<div>
|
|
||||||
<span class="font-semibold">{gettext("Preview Mockup")}</span>
|
|
||||||
<span class="text-sm text-base-content/70 ml-2">
|
|
||||||
– {gettext("This page is not functional and only displays the planned features.")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
# Info card explaining the contribution type concept
|
|
||||||
defp info_card(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div class="card bg-base-200 mt-6">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title">
|
|
||||||
<.icon name="hero-information-circle" class="size-5" />
|
|
||||||
{gettext("About Contribution Types")}
|
|
||||||
</h2>
|
|
||||||
<div class="prose prose-sm max-w-none">
|
|
||||||
<p>
|
|
||||||
{gettext(
|
|
||||||
"Contribution types define different membership fee structures. Each type has a fixed cycle (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<strong>{gettext("Name & Amount")}</strong>
|
|
||||||
- {gettext("Can be changed at any time. Amount changes affect future periods only.")}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>{gettext("Interval")}</strong>
|
|
||||||
- {gettext(
|
|
||||||
"Fixed after creation. Members can only switch between types with the same interval."
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>{gettext("Deletion")}</strong>
|
|
||||||
- {gettext("Only possible if no members are assigned to this type.")}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
# Mock data for demonstration
|
|
||||||
defp mock_contribution_types do
|
|
||||||
[
|
|
||||||
%{
|
|
||||||
id: "1",
|
|
||||||
name: gettext("Regular"),
|
|
||||||
description: gettext("Standard membership fee for regular members"),
|
|
||||||
amount: Decimal.new("60.00"),
|
|
||||||
interval: :yearly,
|
|
||||||
member_count: 45
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
id: "2",
|
|
||||||
name: gettext("Reduced"),
|
|
||||||
description: gettext("Reduced fee for unemployed, pensioners, or low income"),
|
|
||||||
amount: Decimal.new("30.00"),
|
|
||||||
interval: :yearly,
|
|
||||||
member_count: 12
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
id: "3",
|
|
||||||
name: gettext("Student"),
|
|
||||||
description: gettext("Monthly fee for students and trainees"),
|
|
||||||
amount: Decimal.new("5.00"),
|
|
||||||
interval: :monthly,
|
|
||||||
member_count: 8
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
id: "4",
|
|
||||||
name: gettext("Family"),
|
|
||||||
description: gettext("Quarterly fee for family memberships"),
|
|
||||||
amount: Decimal.new("25.00"),
|
|
||||||
interval: :quarterly,
|
|
||||||
member_count: 15
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
id: "5",
|
|
||||||
name: gettext("Supporting Member"),
|
|
||||||
description: gettext("Half-yearly contribution for supporting members"),
|
|
||||||
amount: Decimal.new("100.00"),
|
|
||||||
interval: :half_yearly,
|
|
||||||
member_count: 3
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
id: "6",
|
|
||||||
name: gettext("Honorary"),
|
|
||||||
description: gettext("No fee for honorary members"),
|
|
||||||
amount: Decimal.new("0.00"),
|
|
||||||
interval: :yearly,
|
|
||||||
member_count: 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
defp format_currency(%Decimal{} = amount) do
|
|
||||||
"#{Decimal.to_string(amount)} €"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp format_interval(:monthly), do: gettext("Monthly")
|
|
||||||
defp format_interval(:quarterly), do: gettext("Quarterly")
|
|
||||||
defp format_interval(:half_yearly), do: gettext("Half-yearly")
|
|
||||||
defp format_interval(:yearly), do: gettext("Yearly")
|
|
||||||
end
|
|
||||||
|
|
@ -1,300 +0,0 @@
|
||||||
defmodule MvWeb.CustomFieldValueLive.Form do
|
|
||||||
@moduledoc """
|
|
||||||
LiveView form for creating and editing custom field values.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
- 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 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 custom field type:
|
|
||||||
- String: text input
|
|
||||||
- Integer: number input
|
|
||||||
- Boolean: checkbox
|
|
||||||
- Date: date picker
|
|
||||||
- Email: email input with validation
|
|
||||||
|
|
||||||
## Events
|
|
||||||
- `validate` - Real-time form validation
|
|
||||||
- `save` - Submit form (create or update custom field value)
|
|
||||||
|
|
||||||
## Note
|
|
||||||
Custom field values are typically managed through the member edit form,
|
|
||||||
not through this standalone form.
|
|
||||||
"""
|
|
||||||
use MvWeb, :live_view
|
|
||||||
|
|
||||||
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
|
||||||
import MvWeb.LiveHelpers, only: [current_actor: 1, submit_form: 3]
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
|
||||||
<.header>
|
|
||||||
{@page_title}
|
|
||||||
<:subtitle>
|
|
||||||
{gettext("Use this form to manage Custom Field Value records in your database.")}
|
|
||||||
</:subtitle>
|
|
||||||
</.header>
|
|
||||||
|
|
||||||
<.form for={@form} id="custom_field_value-form" phx-change="validate" phx-submit="save">
|
|
||||||
<!-- Custom Field Selection -->
|
|
||||||
<.input
|
|
||||||
field={@form[:custom_field_id]}
|
|
||||||
type="select"
|
|
||||||
label={gettext("Custom field")}
|
|
||||||
options={custom_field_options(@custom_fields)}
|
|
||||||
prompt={gettext("Choose a custom field")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Member Selection -->
|
|
||||||
<.input
|
|
||||||
field={@form[:member_id]}
|
|
||||||
type="select"
|
|
||||||
label={gettext("Member")}
|
|
||||||
options={member_options(@members)}
|
|
||||||
prompt={gettext("Choose a member")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Value Input - handles Union 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 custom field first")}
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
|
||||||
{gettext("Save Custom Field Value")}
|
|
||||||
</.button>
|
|
||||||
<.button navigate={return_path(@return_to, @custom_field_value)}>{gettext("Cancel")}</.button>
|
|
||||||
</.form>
|
|
||||||
</Layouts.app>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper function for Union-Value Input
|
|
||||||
defp union_value_input(assigns) do
|
|
||||||
# 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"""
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="block text-sm font-medium text-gray-700">
|
|
||||||
{gettext("Value")}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<%= 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} />
|
|
||||||
<input type="hidden" name={value_form[:_union_type].name} value="string" />
|
|
||||||
</.inputs_for>
|
|
||||||
<% :integer -> %>
|
|
||||||
<.inputs_for :let={value_form} field={@form[:value]}>
|
|
||||||
<.input field={value_form[:value]} type="number" label="" value={@current_value} />
|
|
||||||
<input type="hidden" name={value_form[:_union_type].name} value="integer" />
|
|
||||||
</.inputs_for>
|
|
||||||
<% :boolean -> %>
|
|
||||||
<.inputs_for :let={value_form} field={@form[:value]}>
|
|
||||||
<.input field={value_form[:value]} type="checkbox" label="" checked={@current_value} />
|
|
||||||
<input type="hidden" name={value_form[:_union_type].name} value="boolean" />
|
|
||||||
</.inputs_for>
|
|
||||||
<% :date -> %>
|
|
||||||
<.inputs_for :let={value_form} field={@form[:value]}>
|
|
||||||
<.input
|
|
||||||
field={value_form[:value]}
|
|
||||||
type="date"
|
|
||||||
label=""
|
|
||||||
value={format_date_value(@current_value)}
|
|
||||||
/>
|
|
||||||
<input type="hidden" name={value_form[:_union_type].name} value="date" />
|
|
||||||
</.inputs_for>
|
|
||||||
<% :email -> %>
|
|
||||||
<.inputs_for :let={value_form} field={@form[:value]}>
|
|
||||||
<.input field={value_form[:value]} type="email" label="" value={@current_value} />
|
|
||||||
<input type="hidden" name={value_form[:_union_type].name} value="email" />
|
|
||||||
</.inputs_for>
|
|
||||||
<% _ -> %>
|
|
||||||
<div class="text-sm text-red-600">
|
|
||||||
{gettext("Unsupported value type: %{type}", type: @custom_field.value_type)}
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper function to extract the current value from the CustomFieldValue
|
|
||||||
defp extract_current_value(
|
|
||||||
%Mv.Membership.CustomFieldValue{value: %Ash.Union{value: value}},
|
|
||||||
_value_type
|
|
||||||
) do
|
|
||||||
value
|
|
||||||
end
|
|
||||||
|
|
||||||
defp extract_current_value(_data, _value_type) do
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper function to format Date values for HTML input
|
|
||||||
defp format_date_value(%Date{} = date) do
|
|
||||||
Date.to_iso8601(date)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp format_date_value(nil), do: ""
|
|
||||||
|
|
||||||
defp format_date_value(date) when is_binary(date) do
|
|
||||||
case Date.from_iso8601(date) do
|
|
||||||
{:ok, parsed_date} -> Date.to_iso8601(parsed_date)
|
|
||||||
_ -> ""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp format_date_value(_), do: ""
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def mount(params, _session, socket) do
|
|
||||||
custom_field_value =
|
|
||||||
case params["id"] do
|
|
||||||
nil -> nil
|
|
||||||
id -> Ash.get!(Mv.Membership.CustomFieldValue, id) |> Ash.load!([:custom_field])
|
|
||||||
end
|
|
||||||
|
|
||||||
action = if is_nil(custom_field_value), do: "New", else: "Edit"
|
|
||||||
page_title = action <> " " <> "Custom field value"
|
|
||||||
|
|
||||||
# Load all CustomFields and Members for the selection fields
|
|
||||||
actor = current_actor(socket)
|
|
||||||
custom_fields = Ash.read!(Mv.Membership.CustomField, actor: actor)
|
|
||||||
members = Ash.read!(Mv.Membership.Member, actor: actor)
|
|
||||||
|
|
||||||
{:ok,
|
|
||||||
socket
|
|
||||||
|> assign(:return_to, return_to(params["return_to"]))
|
|
||||||
|> assign(custom_field_value: custom_field_value)
|
|
||||||
|> assign(:page_title, page_title)
|
|
||||||
|> assign(:custom_fields, custom_fields)
|
|
||||||
|> assign(:members, members)
|
|
||||||
|> assign(:selected_custom_field, custom_field_value && custom_field_value.custom_field)
|
|
||||||
|> assign_form()}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp return_to("show"), do: "show"
|
|
||||||
defp return_to(_), do: "index"
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
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.custom_fields, &(&1.id == id))
|
|
||||||
end
|
|
||||||
|
|
||||||
# Set the Union type based on the selected CustomField
|
|
||||||
updated_params =
|
|
||||||
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
|
|
||||||
custom_field_value_params
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> assign(:selected_custom_field, selected_custom_field)
|
|
||||||
|> assign(form: AshPhoenix.Form.validate(socket.assigns.form, updated_params))}
|
|
||||||
end
|
|
||||||
|
|
||||||
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_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
|
|
||||||
custom_field_value_params
|
|
||||||
end
|
|
||||||
|
|
||||||
actor = current_actor(socket)
|
|
||||||
|
|
||||||
case submit_form(socket.assigns.form, updated_params, actor) do
|
|
||||||
{:ok, custom_field_value} ->
|
|
||||||
notify_parent({:saved, custom_field_value})
|
|
||||||
|
|
||||||
action =
|
|
||||||
case socket.assigns.form.source.type do
|
|
||||||
:create -> gettext("create")
|
|
||||||
:update -> gettext("update")
|
|
||||||
other -> to_string(other)
|
|
||||||
end
|
|
||||||
|
|
||||||
socket =
|
|
||||||
socket
|
|
||||||
|> 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}
|
|
||||||
|
|
||||||
{:error, form} ->
|
|
||||||
{:noreply, assign(socket, form: form)}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
|
||||||
|
|
||||||
defp assign_form(%{assigns: %{custom_field_value: custom_field_value}} = socket) do
|
|
||||||
form =
|
|
||||||
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
|
|
||||||
%{"value" => %{"_union_type" => to_string(union_type)}}
|
|
||||||
else
|
|
||||||
%{}
|
|
||||||
end
|
|
||||||
|
|
||||||
AshPhoenix.Form.for_update(custom_field_value, :update,
|
|
||||||
as: "custom_field_value",
|
|
||||||
params: params
|
|
||||||
)
|
|
||||||
else
|
|
||||||
AshPhoenix.Form.for_create(Mv.Membership.CustomFieldValue, :create,
|
|
||||||
as: "custom_field_value"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
assign(socket, form: to_form(form))
|
|
||||||
end
|
|
||||||
|
|
||||||
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 custom_field_options(custom_fields) do
|
|
||||||
Enum.map(custom_fields, &{&1.name, &1.id})
|
|
||||||
end
|
|
||||||
|
|
||||||
defp member_options(members) do
|
|
||||||
Enum.map(members, &{MvWeb.Helpers.MemberHelpers.display_name(&1), &1.id})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,157 +0,0 @@
|
||||||
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
|
|
||||||
|
|
||||||
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
|
||||||
import MvWeb.LiveHelpers, only: [current_actor: 1]
|
|
||||||
|
|
||||||
@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
|
|
||||||
actor = current_actor(socket)
|
|
||||||
|
|
||||||
# Early return if no actor (prevents exceptions in unauthenticated tests)
|
|
||||||
if is_nil(actor) do
|
|
||||||
{:ok,
|
|
||||||
socket
|
|
||||||
|> assign(:page_title, "Listing Custom field values")
|
|
||||||
|> stream(:custom_field_values, [])}
|
|
||||||
else
|
|
||||||
case Ash.read(Mv.Membership.CustomFieldValue, actor: actor) do
|
|
||||||
{:ok, custom_field_values} ->
|
|
||||||
{:ok,
|
|
||||||
socket
|
|
||||||
|> assign(:page_title, "Listing Custom field values")
|
|
||||||
|> stream(:custom_field_values, custom_field_values)}
|
|
||||||
|
|
||||||
{:error, %Ash.Error.Forbidden{}} ->
|
|
||||||
{:ok,
|
|
||||||
socket
|
|
||||||
|> assign(:page_title, "Listing Custom field values")
|
|
||||||
|> stream(:custom_field_values, [])
|
|
||||||
|> put_flash(:error, gettext("You do not have permission to view custom field values"))}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:ok,
|
|
||||||
socket
|
|
||||||
|> assign(:page_title, "Listing Custom field values")
|
|
||||||
|> stream(:custom_field_values, [])
|
|
||||||
|> put_flash(:error, format_error(error))}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_event("delete", %{"id" => id}, socket) do
|
|
||||||
actor = MvWeb.LiveHelpers.current_actor(socket)
|
|
||||||
|
|
||||||
case Ash.get(Mv.Membership.CustomFieldValue, id, actor: actor) do
|
|
||||||
{:ok, custom_field_value} ->
|
|
||||||
case Ash.destroy(custom_field_value, actor: actor) do
|
|
||||||
:ok ->
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> stream_delete(:custom_field_values, custom_field_value)
|
|
||||||
|> put_flash(:info, gettext("Custom field value deleted successfully"))}
|
|
||||||
|
|
||||||
{:error, %Ash.Error.Forbidden{}} ->
|
|
||||||
{:noreply,
|
|
||||||
put_flash(
|
|
||||||
socket,
|
|
||||||
:error,
|
|
||||||
gettext("You do not have permission to delete this custom field value")
|
|
||||||
)}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:noreply, put_flash(socket, :error, format_error(error))}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, %Ash.Error.Query.NotFound{}} ->
|
|
||||||
{:noreply, put_flash(socket, :error, gettext("Custom field value not found"))}
|
|
||||||
|
|
||||||
{:error, %Ash.Error.Forbidden{} = _error} ->
|
|
||||||
{:noreply,
|
|
||||||
put_flash(
|
|
||||||
socket,
|
|
||||||
:error,
|
|
||||||
gettext("You do not have permission to access this custom field value")
|
|
||||||
)}
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
{:noreply, put_flash(socket, :error, format_error(error))}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp format_error(%Ash.Error.Invalid{errors: errors}) do
|
|
||||||
Enum.map_join(errors, ", ", fn %{message: message} -> message end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp format_error(error) do
|
|
||||||
inspect(error)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
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>
|
|
||||||
Data 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 data field value"
|
|
||||||
defp page_title(:edit), do: "Edit data field value"
|
|
||||||
end
|
|
||||||
|
|
@ -13,7 +13,7 @@ defmodule MvWeb.MemberLive.Form do
|
||||||
## Form Sections
|
## Form Sections
|
||||||
- Personal Data: Name, address, contact information, membership dates, notes
|
- Personal Data: Name, address, contact information, membership dates, notes
|
||||||
- Custom Fields: Dynamic fields in uniform grid layout (displayed sorted by name)
|
- Custom Fields: Dynamic fields in uniform grid layout (displayed sorted by name)
|
||||||
- Payment Data: Mockup section (not editable)
|
- Membership Fee: Selection of membership fee type with interval validation
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
- `validate` - Real-time form validation
|
- `validate` - Real-time form validation
|
||||||
|
|
@ -355,55 +355,72 @@ defmodule MvWeb.MemberLive.Form do
|
||||||
|
|
||||||
# Extracts a user-friendly error message from form errors
|
# Extracts a user-friendly error message from form errors
|
||||||
defp extract_error_message(form) do
|
defp extract_error_message(form) do
|
||||||
# Try to extract message from source errors first
|
|
||||||
source_errors = get_source_errors(form)
|
source_errors = get_source_errors(form)
|
||||||
|
|
||||||
case source_errors do
|
cond do
|
||||||
[%Ash.Error.Invalid{errors: errors} | _] when is_list(errors) ->
|
has_invalid_error?(source_errors) ->
|
||||||
# Extract first error message
|
extract_invalid_error_message(source_errors)
|
||||||
case List.first(errors) do
|
|
||||||
%{message: message} when is_binary(message) ->
|
|
||||||
gettext("Validation failed: %{message}", message: message)
|
|
||||||
|
|
||||||
%{field: field, message: message} when is_binary(message) ->
|
has_other_error?(source_errors) ->
|
||||||
gettext("Validation failed: %{field} %{message}", field: field, message: message)
|
extract_other_error_message(source_errors)
|
||||||
|
|
||||||
_ ->
|
has_form_errors?(form) ->
|
||||||
gettext("Validation failed. Please check your input.")
|
gettext("Please correct the errors in the form and try again.")
|
||||||
end
|
|
||||||
|
|
||||||
[error | _] ->
|
true ->
|
||||||
# Try to extract message from other error types
|
gettext("Failed to save member. Please try again.")
|
||||||
case error do
|
end
|
||||||
%{message: message} when is_binary(message) ->
|
end
|
||||||
message
|
|
||||||
|
|
||||||
error when is_struct(error) ->
|
# Checks if source errors contain an Ash.Error.Invalid
|
||||||
# Try to use Ash.ErrorKind protocol if available
|
defp has_invalid_error?([%Ash.Error.Invalid{errors: errors} | _]) when is_list(errors), do: true
|
||||||
try do
|
defp has_invalid_error?(_), do: false
|
||||||
Ash.ErrorKind.message(error)
|
|
||||||
rescue
|
|
||||||
Protocol.UndefinedError -> gettext("Failed to save member. Please try again.")
|
|
||||||
end
|
|
||||||
|
|
||||||
_ ->
|
# Extracts message from Ash.Error.Invalid
|
||||||
gettext("Failed to save member. Please try again.")
|
defp extract_invalid_error_message([%Ash.Error.Invalid{errors: errors} | _]) do
|
||||||
end
|
case List.first(errors) do
|
||||||
|
%{message: message} when is_binary(message) ->
|
||||||
|
gettext("Validation failed: %{message}", message: message)
|
||||||
|
|
||||||
|
%{field: field, message: message} when is_binary(message) ->
|
||||||
|
gettext("Validation failed: %{field} %{message}", field: field, message: message)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
# Check if there are any field errors in the form
|
gettext("Validation failed. Please check your input.")
|
||||||
if has_form_errors?(form) do
|
end
|
||||||
gettext("Please correct the errors in the form and try again.")
|
end
|
||||||
else
|
|
||||||
gettext("Failed to save member. Please try again.")
|
# Checks if source errors contain other error types
|
||||||
end
|
defp has_other_error?([_ | _]), do: true
|
||||||
|
defp has_other_error?(_), do: false
|
||||||
|
|
||||||
|
# Extracts message from other error types
|
||||||
|
defp extract_other_error_message([error | _]) do
|
||||||
|
cond do
|
||||||
|
Map.has_key?(error, :message) and is_binary(error.message) ->
|
||||||
|
error.message
|
||||||
|
|
||||||
|
is_struct(error) ->
|
||||||
|
extract_struct_error_message(error)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
gettext("Failed to save member. Please try again.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Extracts message from struct error using Ash.ErrorKind protocol
|
||||||
|
defp extract_struct_error_message(error) do
|
||||||
|
try do
|
||||||
|
Ash.ErrorKind.message(error)
|
||||||
|
rescue
|
||||||
|
Protocol.UndefinedError -> gettext("Failed to save member. Please try again.")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Checks if form has any errors
|
# Checks if form has any errors
|
||||||
defp has_form_errors?(form) do
|
defp has_form_errors?(form) do
|
||||||
case Map.get(form, :errors) do
|
case Map.get(form, :errors) do
|
||||||
errors when is_list(errors) and length(errors) > 0 -> true
|
errors when is_list(errors) and errors != [] -> true
|
||||||
_ -> false
|
_ -> false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ defmodule MvWeb.MemberLive.Show do
|
||||||
## Sections
|
## Sections
|
||||||
- Personal Data: Name, address, contact information, membership dates, notes
|
- Personal Data: Name, address, contact information, membership dates, notes
|
||||||
- Custom Fields: Dynamic fields in uniform grid layout (sorted by name)
|
- Custom Fields: Dynamic fields in uniform grid layout (sorted by name)
|
||||||
- Payment Data: Mockup section with placeholder data
|
- Membership Fees: Tab showing all membership fee cycles with status management (via MembershipFeesComponent)
|
||||||
|
|
||||||
## Navigation
|
## Navigation
|
||||||
- Back to member list
|
- Back to member list
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,6 @@ defmodule MvWeb.Router do
|
||||||
live "/members/:id", MemberLive.Show, :show
|
live "/members/:id", MemberLive.Show, :show
|
||||||
live "/members/:id/show/edit", MemberLive.Show, :edit
|
live "/members/:id/show/edit", MemberLive.Show, :edit
|
||||||
|
|
||||||
live "/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", UserLive.Index, :index
|
||||||
live "/users/new", UserLive.Form, :new
|
live "/users/new", UserLive.Form, :new
|
||||||
live "/users/:id/edit", UserLive.Form, :edit
|
live "/users/:id/edit", UserLive.Form, :edit
|
||||||
|
|
@ -80,10 +74,6 @@ defmodule MvWeb.Router do
|
||||||
live "/membership_fee_types/new", MembershipFeeTypeLive.Form, :new
|
live "/membership_fee_types/new", MembershipFeeTypeLive.Form, :new
|
||||||
live "/membership_fee_types/:id/edit", MembershipFeeTypeLive.Form, :edit
|
live "/membership_fee_types/:id/edit", MembershipFeeTypeLive.Form, :edit
|
||||||
|
|
||||||
# Contribution Management (Mock-ups)
|
|
||||||
live "/contribution_types", ContributionTypeLive.Index, :index
|
|
||||||
live "/contributions/member/:id", ContributionPeriodLive.Show, :show
|
|
||||||
|
|
||||||
# Role Management (Admin only)
|
# Role Management (Admin only)
|
||||||
live "/admin/roles", RoleLive.Index, :index
|
live "/admin/roles", RoleLive.Index, :index
|
||||||
live "/admin/roles/new", RoleLive.Form, :new
|
live "/admin/roles/new", RoleLive.Form, :new
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ msgstr ""
|
||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
|
|
||||||
#: lib/mv_web/components/core_components.ex
|
#: lib/mv_web/components/core_components.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr "Aktionen"
|
msgstr "Aktionen"
|
||||||
|
|
@ -37,7 +36,6 @@ msgstr "Verbindung wird wiederhergestellt"
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr "Stadt"
|
msgstr "Stadt"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
|
|
@ -47,7 +45,6 @@ msgstr "Stadt"
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr "Löschen"
|
msgstr "Löschen"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_field_live/index_component.ex
|
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
|
@ -65,7 +62,6 @@ msgstr "Bearbeiten"
|
||||||
msgid "Edit Member"
|
msgid "Edit Member"
|
||||||
msgstr "Mitglied bearbeiten"
|
msgstr "Mitglied bearbeiten"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
|
|
@ -141,7 +137,6 @@ msgstr "Austrittsdatum"
|
||||||
msgid "House Number"
|
msgid "House Number"
|
||||||
msgstr "Hausnummer"
|
msgstr "Hausnummer"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/translations/member_fields.ex
|
#: lib/mv_web/translations/member_fields.ex
|
||||||
|
|
@ -150,7 +145,6 @@ msgid "Notes"
|
||||||
msgstr "Notizen"
|
msgstr "Notizen"
|
||||||
|
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||||
|
|
@ -171,7 +165,6 @@ msgid "Save Member"
|
||||||
msgstr "Mitglied speichern"
|
msgstr "Mitglied speichern"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#: lib/mv_web/live/global_settings_live.ex
|
#: lib/mv_web/live/global_settings_live.ex
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
|
@ -214,14 +207,12 @@ msgid "Yes"
|
||||||
msgstr "Ja"
|
msgstr "Ja"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "create"
|
msgid "create"
|
||||||
msgstr "erstellt"
|
msgstr "erstellt"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "update"
|
msgid "update"
|
||||||
|
|
@ -264,7 +255,6 @@ msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
|
|
@ -275,11 +265,6 @@ msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt"
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Abbrechen"
|
msgstr "Abbrechen"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Choose a member"
|
|
||||||
msgstr "Mitglied auswählen"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
|
|
@ -313,13 +298,7 @@ msgstr "Abmelden"
|
||||||
msgid "Listing Users"
|
msgid "Listing Users"
|
||||||
msgstr "Benutzer*innen auflisten"
|
msgstr "Benutzer*innen auflisten"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Member"
|
|
||||||
msgstr "Mitglied"
|
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/sidebar.ex
|
#: lib/mv_web/components/layouts/sidebar.ex
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/member_live/index.ex
|
#: lib/mv_web/live/member_live/index.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
|
|
@ -327,7 +306,6 @@ msgstr "Mitglied"
|
||||||
msgid "Members"
|
msgid "Members"
|
||||||
msgstr "Mitglieder"
|
msgstr "Mitglieder"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
|
|
@ -351,7 +329,6 @@ msgstr "Neue*r Benutzer*in"
|
||||||
msgid "Not enabled"
|
msgid "Not enabled"
|
||||||
msgstr "Nicht aktiviert"
|
msgstr "Nicht aktiviert"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/user_live/form.ex
|
#: lib/mv_web/live/user_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Note"
|
msgid "Note"
|
||||||
|
|
@ -401,11 +378,6 @@ msgstr "Benutzer*in anzeigen"
|
||||||
msgid "This is a user record from your database."
|
msgid "This is a user record from your database."
|
||||||
msgstr "Dies ist ein Benutzer*innen-Datensatz aus Ihrer Datenbank."
|
msgstr "Dies ist ein Benutzer*innen-Datensatz aus Ihrer Datenbank."
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Unsupported value type: %{type}"
|
|
||||||
msgstr "Nicht unterstützter Wertetyp: %{type}"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/user_live/form.ex
|
#: lib/mv_web/live/user_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Use this form to manage user records in your database."
|
msgid "Use this form to manage user records in your database."
|
||||||
|
|
@ -417,11 +389,6 @@ msgstr "Verwenden Sie dieses Formular, um Benutzer*innen-Datensätze zu verwalte
|
||||||
msgid "User"
|
msgid "User"
|
||||||
msgstr "Benutzer*in"
|
msgstr "Benutzer*in"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Value"
|
|
||||||
msgstr "Wert"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -611,37 +578,12 @@ msgstr "E-Mail kann nicht aktualisiert werden: Diese E-Mail-Adresse ist bereits
|
||||||
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
|
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
|
||||||
msgstr "Diese E-Mail-Adresse ist bereits mit einem anderen OIDC-Konto verknüpft. Es können nicht mehrere OIDC-Provider mit demselben Konto verknüpft werden."
|
msgstr "Diese E-Mail-Adresse ist bereits mit einem anderen OIDC-Konto verknüpft. Es können nicht mehrere OIDC-Provider mit demselben Konto verknüpft werden."
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Choose a custom field"
|
|
||||||
msgstr "Wähle ein Benutzerdefiniertes Feld"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Custom field"
|
|
||||||
msgstr "Benutzerdefinierte Felder"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, 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
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Please select a custom field first"
|
|
||||||
msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Custom Fields"
|
msgid "Custom Fields"
|
||||||
msgstr "Benutzerdefinierte Felder"
|
msgstr "Benutzerdefinierte Felder"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
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/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "%{count} member has a value assigned for this custom field."
|
msgid "%{count} member has a value assigned for this custom field."
|
||||||
|
|
@ -866,20 +808,6 @@ msgstr "Speichern"
|
||||||
msgid "Create Member"
|
msgid "Create Member"
|
||||||
msgstr "Mitglied erstellen"
|
msgstr "Mitglied erstellen"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "%{count} period selected"
|
|
||||||
msgid_plural "%{count} periods selected"
|
|
||||||
msgstr[0] "%{count} Zyklus ausgewählt"
|
|
||||||
msgstr[1] "%{count} Zyklen ausgewählt"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "About Contribution Types"
|
|
||||||
msgstr "Über Beitragsarten"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
|
|
@ -887,54 +815,16 @@ msgstr "Über Beitragsarten"
|
||||||
msgid "Amount"
|
msgid "Amount"
|
||||||
msgstr "Betrag"
|
msgstr "Betrag"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Back to Settings"
|
msgid "Back to Settings"
|
||||||
msgstr "Zurück zu den Einstellungen"
|
msgstr "Zurück zu den Einstellungen"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Can be changed at any time. Amount changes affect future periods only."
|
msgid "Can be changed at any time. Amount changes affect future periods only."
|
||||||
msgstr "Kann jederzeit geändert werden. Änderungen des Betrags betreffen nur zukünftige Zyklen."
|
msgstr "Kann jederzeit geändert werden. Änderungen des Betrags betreffen nur zukünftige Zyklen."
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Cannot delete - members assigned"
|
|
||||||
msgstr "Löschen nicht möglich – es sind Mitglieder zugewiesen"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Change Contribution Type"
|
|
||||||
msgstr "Beitragsart ändern"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Contribution Start"
|
|
||||||
msgstr "Beitragsbeginn"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Contribution Types"
|
|
||||||
msgstr "Beitragsarten"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Contribution type"
|
|
||||||
msgstr "Beitragsart"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Contributions for %{name}"
|
|
||||||
msgstr "Beiträge für %{name}"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Current"
|
|
||||||
msgstr "Aktuell"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Deletion"
|
msgid "Deletion"
|
||||||
|
|
@ -945,12 +835,6 @@ msgstr "Löschen"
|
||||||
msgid "Examples"
|
msgid "Examples"
|
||||||
msgstr "Beispiele"
|
msgstr "Beispiele"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Family"
|
|
||||||
msgstr "Familie"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Fixed after creation. Members can only switch between types with the same interval."
|
msgid "Fixed after creation. Members can only switch between types with the same interval."
|
||||||
|
|
@ -962,27 +846,12 @@ msgid "Global Settings"
|
||||||
msgstr "Globale Einstellungen"
|
msgstr "Globale Einstellungen"
|
||||||
|
|
||||||
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_settings_live.ex
|
#: lib/mv_web/live/membership_fee_settings_live.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Half-yearly"
|
msgid "Half-yearly"
|
||||||
msgstr "Halbjährlich"
|
msgstr "Halbjährlich"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Half-yearly contribution for supporting members"
|
|
||||||
msgstr "Halbjährlicher Beitrag für Fördermitglieder"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Honorary"
|
|
||||||
msgstr "Ehrenamtlich"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
|
|
@ -995,36 +864,6 @@ msgstr "Intervall"
|
||||||
msgid "Joining date"
|
msgid "Joining date"
|
||||||
msgstr "Beitrittsdatum"
|
msgstr "Beitrittsdatum"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Joining year - reduced to 0"
|
|
||||||
msgstr "Beitrittsjahr – auf 0 reduziert"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Manage contribution types for membership fees."
|
|
||||||
msgstr "Beitragsarten für Mitgliedsbeiträge verwalten."
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Mark as Paid"
|
|
||||||
msgstr "Als bezahlt markieren"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Mark as Suspended"
|
|
||||||
msgstr "Als pausiert markieren"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Mark as Unpaid"
|
|
||||||
msgstr "Als unbezahlt markieren"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Member Contributions"
|
|
||||||
msgstr "Mitgliedsbeiträge"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_settings_live.ex
|
#: lib/mv_web/live/membership_fee_settings_live.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Member pays for the year they joined"
|
msgid "Member pays for the year they joined"
|
||||||
|
|
@ -1045,131 +884,35 @@ msgstr "Mitglied zahlt ab dem nächsten vollständigen Quartal"
|
||||||
msgid "Member pays from the next full year"
|
msgid "Member pays from the next full year"
|
||||||
msgstr "Mitglied zahlt ab dem nächsten vollständigen Jahr"
|
msgstr "Mitglied zahlt ab dem nächsten vollständigen Jahr"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Member since"
|
|
||||||
msgstr "Mitglied seit"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Members can only switch between contribution types with the same payment interval (e.g., yearly to yearly). This prevents complex period overlaps."
|
|
||||||
msgstr "Mitglieder können nur zwischen Beitragsarten mit demselben Zahlungszyklus wechseln (z. B. jährlich zu jährlich). Dadurch werden komplexe Überlappungen vermieden."
|
|
||||||
|
|
||||||
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_settings_live.ex
|
#: lib/mv_web/live/membership_fee_settings_live.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Monthly"
|
msgid "Monthly"
|
||||||
msgstr "Monatlich"
|
msgstr "Monatlich"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Monthly fee for students and trainees"
|
|
||||||
msgstr "Monatlicher Beitrag für Studierende und Auszubildende"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Name & Amount"
|
msgid "Name & Amount"
|
||||||
msgstr "Name & Betrag"
|
msgstr "Name & Betrag"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "New Contribution Type"
|
|
||||||
msgstr "Neue Beitragsart"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "No fee for honorary members"
|
|
||||||
msgstr "Kein Beitrag für ehrenamtliche Mitglieder"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Only possible if no members are assigned to this type."
|
msgid "Only possible if no members are assigned to this type."
|
||||||
msgstr "Nur möglich, wenn diesem Typ keine Mitglieder zugewiesen sind."
|
msgstr "Nur möglich, wenn diesem Typ keine Mitglieder zugewiesen sind."
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Open Contributions"
|
|
||||||
msgstr "Offene Beiträge"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Paid via bank transfer"
|
|
||||||
msgstr "Bezahlt durch Überweisung"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Preview Mockup"
|
|
||||||
msgstr "Vorschau"
|
|
||||||
|
|
||||||
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_settings_live.ex
|
#: lib/mv_web/live/membership_fee_settings_live.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Quarterly"
|
msgid "Quarterly"
|
||||||
msgstr "Vierteljährlich"
|
msgstr "Vierteljährlich"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Quarterly fee for family memberships"
|
|
||||||
msgstr "Vierteljährlicher Beitrag für Familienmitgliedschaften"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Reduced"
|
|
||||||
msgstr "Reduziert"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Reduced fee for unemployed, pensioners, or low income"
|
|
||||||
msgstr "Ermäßigter Beitrag für Arbeitslose, Rentner*innen oder Geringverdienende"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Regular"
|
|
||||||
msgstr "Regulär"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Reopen"
|
|
||||||
msgstr "Wieder öffnen"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Standard membership fee for regular members"
|
|
||||||
msgstr "Regulärer Mitgliedsbeitrag für Vollmitglieder"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Status"
|
msgid "Status"
|
||||||
msgstr "Status"
|
msgstr "Status"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Student"
|
|
||||||
msgstr "Student"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Supporting Member"
|
|
||||||
msgstr "Fördermitglied"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Suspend"
|
|
||||||
msgstr "Pausieren"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||||
|
|
@ -1177,24 +920,7 @@ msgstr "Pausieren"
|
||||||
msgid "Suspended"
|
msgid "Suspended"
|
||||||
msgstr "Pausiert"
|
msgstr "Pausiert"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "This page is not functional and only displays the planned features."
|
|
||||||
msgstr "Diese Seite ist nicht funktionsfähig und zeigt nur geplante Funktionen."
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Time Period"
|
|
||||||
msgstr "Zeitraum"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Total Contributions"
|
|
||||||
msgstr "Gesamtbeiträge"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||||
|
|
@ -1202,14 +928,7 @@ msgstr "Gesamtbeiträge"
|
||||||
msgid "Unpaid"
|
msgid "Unpaid"
|
||||||
msgstr "Unbezahlt"
|
msgstr "Unbezahlt"
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Why are not all contribution types shown?"
|
|
||||||
msgstr "Warum werden nicht alle Beitragsarten angezeigt?"
|
|
||||||
|
|
||||||
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_settings_live.ex
|
#: lib/mv_web/live/membership_fee_settings_live.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
|
@ -1697,11 +1416,6 @@ msgstr "Zyklen regenerieren"
|
||||||
msgid "Regenerating..."
|
msgid "Regenerating..."
|
||||||
msgstr "Regeneriere..."
|
msgstr "Regeneriere..."
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Save Custom Field Value"
|
|
||||||
msgstr "Benutzerdefinierten Feldwert speichern"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Save Field"
|
msgid "Save Field"
|
||||||
|
|
@ -1799,11 +1513,6 @@ msgstr "Jährliches Intervall – Beitrittszeitraum einbezogen"
|
||||||
msgid "You are about to delete all %{count} cycles for this member."
|
msgid "You are about to delete all %{count} cycles for this member."
|
||||||
msgstr "Du bist dabei alle %{count} Zyklen für dieses Mitglied zu löschen."
|
msgstr "Du bist dabei alle %{count} Zyklen für dieses Mitglied zu löschen."
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Contribution types define different membership fee structures. Each type has a fixed cycle (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
|
|
||||||
msgstr "Beitragsarten definieren verschiedene Beitragsmodelle. Jede Art hat einen festen Zyklus (monatlich, vierteljährlich, halbjährlich, jährlich), der nach Erstellung nicht mehr geändert werden kann."
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Delete Membership Fee Type"
|
msgid "Delete Membership Fee Type"
|
||||||
|
|
@ -2072,16 +1781,6 @@ msgstr "Zyklus löschen"
|
||||||
msgid "The cycle period will be calculated based on this date and the interval."
|
msgid "The cycle period will be calculated based on this date and the interval."
|
||||||
msgstr "Der Zyklus wird basierend auf diesem Datum und dem Intervall berechnet."
|
msgstr "Der Zyklus wird basierend auf diesem Datum und dem Intervall berechnet."
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Custom field value deleted successfully"
|
|
||||||
msgstr "Benutzerdefinierter Feldwert erfolgreich gelöscht"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Custom field value not found"
|
|
||||||
msgstr "Benutzerdefinierter Feldwert nicht gefunden"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Membership fee type not found"
|
msgid "Membership fee type not found"
|
||||||
|
|
@ -2102,11 +1801,6 @@ msgstr "Benutzer*in erfolgreich gelöscht"
|
||||||
msgid "User not found"
|
msgid "User not found"
|
||||||
msgstr "Benutzer*in nicht gefunden"
|
msgstr "Benutzer*in nicht gefunden"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "You do not have permission to access this custom field value"
|
|
||||||
msgstr "Sie haben keine Berechtigung, auf diesen benutzerdefinierten Feldwert zuzugreifen"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "You do not have permission to access this membership fee type"
|
msgid "You do not have permission to access this membership fee type"
|
||||||
|
|
@ -2117,11 +1811,6 @@ msgstr "Sie haben keine Berechtigung, auf diese Mitgliedsbeitragsart zuzugreifen
|
||||||
msgid "You do not have permission to access this user"
|
msgid "You do not have permission to access this user"
|
||||||
msgstr "Sie haben keine Berechtigung, auf diese*n Benutzer*in zuzugreifen"
|
msgstr "Sie haben keine Berechtigung, auf diese*n Benutzer*in zuzugreifen"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "You do not have permission to delete this custom field value"
|
|
||||||
msgstr "Sie haben keine Berechtigung, diesen benutzerdefinierten Feldwert zu löschen"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "You do not have permission to delete this membership fee type"
|
msgid "You do not have permission to delete this membership fee type"
|
||||||
|
|
@ -2167,11 +1856,6 @@ msgstr "Sie haben keine Berechtigung, auf dieses Mitglied zuzugreifen"
|
||||||
msgid "You do not have permission to delete this member"
|
msgid "You do not have permission to delete this member"
|
||||||
msgstr "Sie haben keine Berechtigung, dieses Mitglied zu löschen"
|
msgstr "Sie haben keine Berechtigung, dieses Mitglied zu löschen"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "You do not have permission to view custom field values"
|
|
||||||
msgstr "Sie haben keine Berechtigung, benutzerdefinierte Feldwerte anzuzeigen"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Member created successfully"
|
msgid "Member created successfully"
|
||||||
|
|
@ -2212,7 +1896,319 @@ msgstr "Beitragstypen"
|
||||||
msgid "Administration"
|
msgid "Administration"
|
||||||
msgstr "Administration"
|
msgstr "Administration"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Failed to %{action} member."
|
||||||
|
msgstr "Fehler beim %{action} des Mitglieds."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Failed to save member. Please try again."
|
||||||
|
msgstr "Fehler beim Speichern des Mitglieds. Bitte versuchen Sie es erneut."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Please correct the errors in the form and try again."
|
||||||
|
msgstr "Bitte korrigieren Sie die Fehler im Formular und versuchen Sie es erneut."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Validation failed. Please check your input."
|
||||||
|
msgstr "Validierung fehlgeschlagen. Bitte überprüfen Sie Ihre Eingabe."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Validation failed: %{field} %{message}"
|
||||||
|
msgstr "Validierung fehlgeschlagen: %{field} %{message}"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Validation failed: %{message}"
|
||||||
|
msgstr "Validierung fehlgeschlagen: %{message}"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ 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/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Member"
|
||||||
|
#~ msgstr "Mitglied"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Choose a custom field"
|
||||||
|
#~ msgstr "Wähle ein Benutzerdefiniertes Feld"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Joining year - reduced to 0"
|
||||||
|
#~ msgstr "Beitrittsjahr – auf 0 reduziert"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Regular"
|
||||||
|
#~ msgstr "Regulär"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Current"
|
||||||
|
#~ msgstr "Aktuell"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Paid via bank transfer"
|
||||||
|
#~ msgstr "Bezahlt durch Überweisung"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Mark as Unpaid"
|
||||||
|
#~ msgstr "Als unbezahlt markieren"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Half-yearly contribution for supporting members"
|
||||||
|
#~ msgstr "Halbjährlicher Beitrag für Fördermitglieder"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Reduced fee for unemployed, pensioners, or low income"
|
||||||
|
#~ msgstr "Ermäßigter Beitrag für Arbeitslose, Rentner*innen oder Geringverdienende"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Custom field value not found"
|
||||||
|
#~ msgstr "Benutzerdefinierter Feldwert nicht gefunden"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Supporting Member"
|
||||||
|
#~ msgstr "Fördermitglied"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Monthly fee for students and trainees"
|
||||||
|
#~ msgstr "Monatlicher Beitrag für Studierende und Auszubildende"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Custom field value %{action} successfully"
|
||||||
|
#~ msgstr "Benutzerdefinierter Feldwert erfolgreich %{action}"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Total Contributions"
|
||||||
|
#~ msgstr "Gesamtbeiträge"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Manage contribution types for membership fees."
|
||||||
|
#~ msgstr "Beitragsarten für Mitgliedsbeiträge verwalten."
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Change Contribution Type"
|
||||||
|
#~ msgstr "Beitragsart ändern"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "New Contribution Type"
|
||||||
|
#~ msgstr "Neue Beitragsart"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Time Period"
|
||||||
|
#~ msgstr "Zeitraum"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Custom field value deleted successfully"
|
||||||
|
#~ msgstr "Benutzerdefinierter Feldwert erfolgreich gelöscht"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "You do not have permission to access this custom field value"
|
||||||
|
#~ msgstr "Sie haben keine Berechtigung, auf diesen benutzerdefinierten Feldwert zuzugreifen"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Cannot delete - members assigned"
|
||||||
|
#~ msgstr "Löschen nicht möglich – es sind Mitglieder zugewiesen"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Preview Mockup"
|
||||||
|
#~ msgstr "Vorschau"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "Contribution Types"
|
||||||
|
#~ msgstr "Beitragsarten"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "This page is not functional and only displays the planned features."
|
||||||
|
#~ msgstr "Diese Seite ist nicht funktionsfähig und zeigt nur geplante Funktionen."
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "Member since"
|
||||||
|
#~ msgstr "Mitglied seit"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Unsupported value type: %{type}"
|
||||||
|
#~ msgstr "Nicht unterstützter Wertetyp: %{type}"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "Custom field"
|
||||||
|
#~ msgstr "Benutzerdefinierte Felder"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Mark as Paid"
|
||||||
|
#~ msgstr "Als bezahlt markieren"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "Contribution type"
|
||||||
|
#~ msgstr "Beitragsart"
|
||||||
|
|
||||||
#~ #: lib/mv_web/components/layouts/sidebar.ex
|
#~ #: lib/mv_web/components/layouts/sidebar.ex
|
||||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
#~ msgid "Contributions"
|
#~ msgid "Contributions"
|
||||||
#~ msgstr "Beiträge"
|
#~ msgstr "Beiträge"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Reduced"
|
||||||
|
#~ msgstr "Reduziert"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "No fee for honorary members"
|
||||||
|
#~ msgstr "Kein Beitrag für ehrenamtliche Mitglieder"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "You do not have permission to delete this custom field value"
|
||||||
|
#~ msgstr "Sie haben keine Berechtigung, diesen benutzerdefinierten Feldwert zu löschen"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "%{count} period selected"
|
||||||
|
#~ msgid_plural "%{count} periods selected"
|
||||||
|
#~ msgstr[0] "%{count} Zyklus ausgewählt"
|
||||||
|
#~ msgstr[1] "%{count} Zyklen ausgewählt"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Mark as Suspended"
|
||||||
|
#~ msgstr "Als pausiert markieren"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "Contribution types define different membership fee structures. Each type has a fixed cycle (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
|
||||||
|
#~ msgstr "Beitragsarten definieren verschiedene Beitragsmodelle. Jede Art hat einen festen Zyklus (monatlich, vierteljährlich, halbjährlich, jährlich), der nach Erstellung nicht mehr geändert werden kann."
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Choose a member"
|
||||||
|
#~ msgstr "Mitglied auswählen"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Suspend"
|
||||||
|
#~ msgstr "Pausieren"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Reopen"
|
||||||
|
#~ msgstr "Wieder öffnen"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Value"
|
||||||
|
#~ msgstr "Wert"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Why are not all contribution types shown?"
|
||||||
|
#~ msgstr "Warum werden nicht alle Beitragsarten angezeigt?"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "Contribution Start"
|
||||||
|
#~ msgstr "Beitragsbeginn"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Standard membership fee for regular members"
|
||||||
|
#~ msgstr "Regulärer Mitgliedsbeitrag für Vollmitglieder"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "Save Custom Field Value"
|
||||||
|
#~ msgstr "Benutzerdefinierten Feldwert speichern"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Honorary"
|
||||||
|
#~ msgstr "Ehrenamtlich"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "Contributions for %{name}"
|
||||||
|
#~ msgstr "Beiträge für %{name}"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Family"
|
||||||
|
#~ msgstr "Familie"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "You do not have permission to view custom field values"
|
||||||
|
#~ msgstr "Sie haben keine Berechtigung, benutzerdefinierte Feldwerte anzuzeigen"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Student"
|
||||||
|
#~ msgstr "Student"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Quarterly fee for family memberships"
|
||||||
|
#~ msgstr "Vierteljährlicher Beitrag für Familienmitgliedschaften"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Members can only switch between contribution types with the same payment interval (e.g., yearly to yearly). This prevents complex period overlaps."
|
||||||
|
#~ msgstr "Mitglieder können nur zwischen Beitragsarten mit demselben Zahlungszyklus wechseln (z. B. jährlich zu jährlich). Dadurch werden komplexe Überlappungen vermieden."
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Please select a custom field first"
|
||||||
|
#~ msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Open Contributions"
|
||||||
|
#~ msgstr "Offene Beiträge"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Member Contributions"
|
||||||
|
#~ msgstr "Mitgliedsbeiträge"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "About Contribution Types"
|
||||||
|
#~ msgstr "Über Beitragsarten"
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/core_components.ex
|
#: lib/mv_web/components/core_components.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -38,7 +37,6 @@ msgstr ""
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
|
|
@ -48,7 +46,6 @@ msgstr ""
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_field_live/index_component.ex
|
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
|
@ -66,7 +63,6 @@ msgstr ""
|
||||||
msgid "Edit Member"
|
msgid "Edit Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
|
|
@ -142,7 +138,6 @@ msgstr ""
|
||||||
msgid "House Number"
|
msgid "House Number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/translations/member_fields.ex
|
#: lib/mv_web/translations/member_fields.ex
|
||||||
|
|
@ -151,7 +146,6 @@ msgid "Notes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||||
|
|
@ -172,7 +166,6 @@ msgid "Save Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#: lib/mv_web/live/global_settings_live.ex
|
#: lib/mv_web/live/global_settings_live.ex
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
|
@ -215,14 +208,12 @@ msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "create"
|
msgid "create"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "update"
|
msgid "update"
|
||||||
|
|
@ -265,7 +256,6 @@ msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
|
|
@ -276,11 +266,6 @@ msgstr ""
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Choose a member"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
|
|
@ -314,13 +299,7 @@ msgstr ""
|
||||||
msgid "Listing Users"
|
msgid "Listing Users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Member"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/sidebar.ex
|
#: lib/mv_web/components/layouts/sidebar.ex
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/member_live/index.ex
|
#: lib/mv_web/live/member_live/index.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
|
|
@ -328,7 +307,6 @@ msgstr ""
|
||||||
msgid "Members"
|
msgid "Members"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
|
|
@ -352,7 +330,6 @@ msgstr ""
|
||||||
msgid "Not enabled"
|
msgid "Not enabled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/user_live/form.ex
|
#: lib/mv_web/live/user_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Note"
|
msgid "Note"
|
||||||
|
|
@ -402,11 +379,6 @@ msgstr ""
|
||||||
msgid "This is a user record from your database."
|
msgid "This is a user record from your database."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Unsupported value type: %{type}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/user_live/form.ex
|
#: lib/mv_web/live/user_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Use this form to manage user records in your database."
|
msgid "Use this form to manage user records in your database."
|
||||||
|
|
@ -418,11 +390,6 @@ msgstr ""
|
||||||
msgid "User"
|
msgid "User"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Value"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -612,37 +579,12 @@ msgstr ""
|
||||||
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
|
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Choose a custom field"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Custom field"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Custom field value %{action} successfully"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Please select a custom field first"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Custom Fields"
|
msgid "Custom Fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Use this form to manage Custom Field Value records in your database."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "%{count} member has a value assigned for this custom field."
|
msgid "%{count} member has a value assigned for this custom field."
|
||||||
|
|
@ -867,20 +809,6 @@ msgstr ""
|
||||||
msgid "Create Member"
|
msgid "Create Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "%{count} period selected"
|
|
||||||
msgid_plural "%{count} periods selected"
|
|
||||||
msgstr[0] ""
|
|
||||||
msgstr[1] ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "About Contribution Types"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
|
|
@ -888,54 +816,16 @@ msgstr ""
|
||||||
msgid "Amount"
|
msgid "Amount"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Back to Settings"
|
msgid "Back to Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Can be changed at any time. Amount changes affect future periods only."
|
msgid "Can be changed at any time. Amount changes affect future periods only."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Cannot delete - members assigned"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Change Contribution Type"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Contribution Start"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Contribution Types"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Contribution type"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Contributions for %{name}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Current"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Deletion"
|
msgid "Deletion"
|
||||||
|
|
@ -946,12 +836,6 @@ msgstr ""
|
||||||
msgid "Examples"
|
msgid "Examples"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Family"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Fixed after creation. Members can only switch between types with the same interval."
|
msgid "Fixed after creation. Members can only switch between types with the same interval."
|
||||||
|
|
@ -963,27 +847,12 @@ msgid "Global Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_settings_live.ex
|
#: lib/mv_web/live/membership_fee_settings_live.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Half-yearly"
|
msgid "Half-yearly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Half-yearly contribution for supporting members"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Honorary"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
|
|
@ -996,36 +865,6 @@ msgstr ""
|
||||||
msgid "Joining date"
|
msgid "Joining date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Joining year - reduced to 0"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Manage contribution types for membership fees."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Mark as Paid"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Mark as Suspended"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Mark as Unpaid"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Member Contributions"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_settings_live.ex
|
#: lib/mv_web/live/membership_fee_settings_live.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Member pays for the year they joined"
|
msgid "Member pays for the year they joined"
|
||||||
|
|
@ -1046,131 +885,35 @@ msgstr ""
|
||||||
msgid "Member pays from the next full year"
|
msgid "Member pays from the next full year"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Member since"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Members can only switch between contribution types with the same payment interval (e.g., yearly to yearly). This prevents complex period overlaps."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_settings_live.ex
|
#: lib/mv_web/live/membership_fee_settings_live.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Monthly"
|
msgid "Monthly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Monthly fee for students and trainees"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Name & Amount"
|
msgid "Name & Amount"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "New Contribution Type"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "No fee for honorary members"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Only possible if no members are assigned to this type."
|
msgid "Only possible if no members are assigned to this type."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Open Contributions"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Paid via bank transfer"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Preview Mockup"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_settings_live.ex
|
#: lib/mv_web/live/membership_fee_settings_live.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Quarterly"
|
msgid "Quarterly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Quarterly fee for family memberships"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Reduced"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Reduced fee for unemployed, pensioners, or low income"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Regular"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Reopen"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Standard membership fee for regular members"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Status"
|
msgid "Status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Student"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Supporting Member"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Suspend"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||||
|
|
@ -1178,24 +921,7 @@ msgstr ""
|
||||||
msgid "Suspended"
|
msgid "Suspended"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "This page is not functional and only displays the planned features."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Time Period"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Total Contributions"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||||
|
|
@ -1203,14 +929,7 @@ msgstr ""
|
||||||
msgid "Unpaid"
|
msgid "Unpaid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Why are not all contribution types shown?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_settings_live.ex
|
#: lib/mv_web/live/membership_fee_settings_live.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -1698,11 +1417,6 @@ msgstr ""
|
||||||
msgid "Regenerating..."
|
msgid "Regenerating..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Save Custom Field Value"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Save Field"
|
msgid "Save Field"
|
||||||
|
|
@ -1800,11 +1514,6 @@ msgstr ""
|
||||||
msgid "You are about to delete all %{count} cycles for this member."
|
msgid "You are about to delete all %{count} cycles for this member."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Contribution types define different membership fee structures. Each type has a fixed cycle (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Delete Membership Fee Type"
|
msgid "Delete Membership Fee Type"
|
||||||
|
|
@ -2073,16 +1782,6 @@ msgstr ""
|
||||||
msgid "The cycle period will be calculated based on this date and the interval."
|
msgid "The cycle period will be calculated based on this date and the interval."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Custom field value deleted successfully"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Custom field value not found"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Membership fee type not found"
|
msgid "Membership fee type not found"
|
||||||
|
|
@ -2103,11 +1802,6 @@ msgstr ""
|
||||||
msgid "User not found"
|
msgid "User not found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "You do not have permission to access this custom field value"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "You do not have permission to access this membership fee type"
|
msgid "You do not have permission to access this membership fee type"
|
||||||
|
|
@ -2118,11 +1812,6 @@ msgstr ""
|
||||||
msgid "You do not have permission to access this user"
|
msgid "You do not have permission to access this user"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "You do not have permission to delete this custom field value"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "You do not have permission to delete this membership fee type"
|
msgid "You do not have permission to delete this membership fee type"
|
||||||
|
|
@ -2168,11 +1857,6 @@ msgstr ""
|
||||||
msgid "You do not have permission to delete this member"
|
msgid "You do not have permission to delete this member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "You do not have permission to view custom field values"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Member created successfully"
|
msgid "Member created successfully"
|
||||||
|
|
@ -2191,6 +1875,7 @@ msgstr ""
|
||||||
#: lib/mv/membership/import/member_csv.ex
|
#: lib/mv/membership/import/member_csv.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Email is required."
|
msgid "Email is required."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/sidebar.ex
|
#: lib/mv_web/components/layouts/sidebar.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -2211,3 +1896,33 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Administration"
|
msgid "Administration"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Failed to %{action} member."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Failed to save member. Please try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Please correct the errors in the form and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Validation failed. Please check your input."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Validation failed: %{field} %{message}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Validation failed: %{message}"
|
||||||
|
msgstr ""
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ msgstr ""
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: lib/mv_web/components/core_components.ex
|
#: lib/mv_web/components/core_components.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -38,7 +37,6 @@ msgstr ""
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
|
|
@ -48,7 +46,6 @@ msgstr ""
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_field_live/index_component.ex
|
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
|
@ -66,7 +63,6 @@ msgstr ""
|
||||||
msgid "Edit Member"
|
msgid "Edit Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
|
|
@ -142,7 +138,6 @@ msgstr ""
|
||||||
msgid "House Number"
|
msgid "House Number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/translations/member_fields.ex
|
#: lib/mv_web/translations/member_fields.ex
|
||||||
|
|
@ -151,7 +146,6 @@ msgid "Notes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||||
|
|
@ -172,7 +166,6 @@ msgid "Save Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#: lib/mv_web/live/global_settings_live.ex
|
#: lib/mv_web/live/global_settings_live.ex
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
|
@ -215,14 +208,12 @@ msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "create"
|
msgid "create"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "update"
|
msgid "update"
|
||||||
|
|
@ -265,7 +256,6 @@ msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
|
|
@ -276,11 +266,6 @@ msgstr ""
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Choose a member"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
|
|
@ -314,13 +299,7 @@ msgstr ""
|
||||||
msgid "Listing Users"
|
msgid "Listing Users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Member"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/sidebar.ex
|
#: lib/mv_web/components/layouts/sidebar.ex
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/member_live/index.ex
|
#: lib/mv_web/live/member_live/index.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
|
|
@ -328,7 +307,6 @@ msgstr ""
|
||||||
msgid "Members"
|
msgid "Members"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
|
|
@ -352,7 +330,6 @@ msgstr ""
|
||||||
msgid "Not enabled"
|
msgid "Not enabled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/user_live/form.ex
|
#: lib/mv_web/live/user_live/form.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Note"
|
msgid "Note"
|
||||||
|
|
@ -402,11 +379,6 @@ msgstr ""
|
||||||
msgid "This is a user record from your database."
|
msgid "This is a user record from your database."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Unsupported value type: %{type}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/user_live/form.ex
|
#: lib/mv_web/live/user_live/form.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Use this form to manage user records in your database."
|
msgid "Use this form to manage user records in your database."
|
||||||
|
|
@ -418,11 +390,6 @@ msgstr ""
|
||||||
msgid "User"
|
msgid "User"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Value"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form_component.ex
|
#: lib/mv_web/live/custom_field_live/form_component.ex
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -612,37 +579,12 @@ msgstr ""
|
||||||
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
|
msgid "This email is already linked to a different OIDC account. Cannot link multiple OIDC providers to the same account."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Choose a custom field"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Custom field"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Custom field value %{action} successfully"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Please select a custom field first"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Custom Fields"
|
msgid "Custom Fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Use this form to manage Custom Field Value records in your database."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "%{count} member has a value assigned for this custom field."
|
msgid "%{count} member has a value assigned for this custom field."
|
||||||
|
|
@ -867,20 +809,6 @@ msgstr ""
|
||||||
msgid "Create Member"
|
msgid "Create Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "%{count} period selected"
|
|
||||||
msgid_plural "%{count} periods selected"
|
|
||||||
msgstr[0] ""
|
|
||||||
msgstr[1] ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "About Contribution Types"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
|
|
@ -888,54 +816,16 @@ msgstr ""
|
||||||
msgid "Amount"
|
msgid "Amount"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Back to Settings"
|
msgid "Back to Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Can be changed at any time. Amount changes affect future periods only."
|
msgid "Can be changed at any time. Amount changes affect future periods only."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Cannot delete - members assigned"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Change Contribution Type"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Contribution Start"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Contribution Types"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Contribution type"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Contributions for %{name}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Current"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Deletion"
|
msgid "Deletion"
|
||||||
|
|
@ -946,12 +836,6 @@ msgstr ""
|
||||||
msgid "Examples"
|
msgid "Examples"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Family"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Fixed after creation. Members can only switch between types with the same interval."
|
msgid "Fixed after creation. Members can only switch between types with the same interval."
|
||||||
|
|
@ -963,27 +847,12 @@ msgid "Global Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_settings_live.ex
|
#: lib/mv_web/live/membership_fee_settings_live.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Half-yearly"
|
msgid "Half-yearly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Half-yearly contribution for supporting members"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Honorary"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
|
|
@ -996,36 +865,6 @@ msgstr ""
|
||||||
msgid "Joining date"
|
msgid "Joining date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Joining year - reduced to 0"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Manage contribution types for membership fees."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Mark as Paid"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Mark as Suspended"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Mark as Unpaid"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Member Contributions"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_settings_live.ex
|
#: lib/mv_web/live/membership_fee_settings_live.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Member pays for the year they joined"
|
msgid "Member pays for the year they joined"
|
||||||
|
|
@ -1046,131 +885,35 @@ msgstr ""
|
||||||
msgid "Member pays from the next full year"
|
msgid "Member pays from the next full year"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Member since"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Members can only switch between contribution types with the same payment interval (e.g., yearly to yearly). This prevents complex period overlaps."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_settings_live.ex
|
#: lib/mv_web/live/membership_fee_settings_live.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Monthly"
|
msgid "Monthly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Monthly fee for students and trainees"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Name & Amount"
|
msgid "Name & Amount"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "New Contribution Type"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "No fee for honorary members"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Only possible if no members are assigned to this type."
|
msgid "Only possible if no members are assigned to this type."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Open Contributions"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Paid via bank transfer"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Preview Mockup"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_settings_live.ex
|
#: lib/mv_web/live/membership_fee_settings_live.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Quarterly"
|
msgid "Quarterly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Quarterly fee for family memberships"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Reduced"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Reduced fee for unemployed, pensioners, or low income"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Regular"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Reopen"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Standard membership fee for regular members"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Status"
|
msgid "Status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Student"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Supporting Member"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Suspend"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||||
|
|
@ -1178,24 +921,7 @@ msgstr ""
|
||||||
msgid "Suspended"
|
msgid "Suspended"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "This page is not functional and only displays the planned features."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Time Period"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Total Contributions"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/components/payment_filter_component.ex
|
#: lib/mv_web/live/components/payment_filter_component.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/member_live/show.ex
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
|
||||||
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
#: lib/mv_web/member_live/index/membership_fee_status.ex
|
||||||
|
|
@ -1203,14 +929,7 @@ msgstr ""
|
||||||
msgid "Unpaid"
|
msgid "Unpaid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Why are not all contribution types shown?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
#: lib/mv_web/helpers/membership_fee_helpers.ex
|
||||||
#: lib/mv_web/live/contribution_period_live/show.ex
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#: lib/mv_web/live/membership_fee_settings_live.ex
|
#: lib/mv_web/live/membership_fee_settings_live.ex
|
||||||
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
#: lib/mv_web/live/membership_fee_type_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -1698,11 +1417,6 @@ msgstr ""
|
||||||
msgid "Regenerating..."
|
msgid "Regenerating..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Save Custom Field Value"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_field_live/form_component.ex
|
#: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Save Field"
|
msgid "Save Field"
|
||||||
|
|
@ -1800,11 +1514,6 @@ msgstr ""
|
||||||
msgid "You are about to delete all %{count} cycles for this member."
|
msgid "You are about to delete all %{count} cycles for this member."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/contribution_type_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Contribution types define different membership fee structures. Each type has a fixed cycle (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Delete Membership Fee Type"
|
msgid "Delete Membership Fee Type"
|
||||||
|
|
@ -2073,16 +1782,6 @@ msgstr ""
|
||||||
msgid "The cycle period will be calculated based on this date and the interval."
|
msgid "The cycle period will be calculated based on this date and the interval."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Custom field value deleted successfully"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "Custom field value not found"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Membership fee type not found"
|
msgid "Membership fee type not found"
|
||||||
|
|
@ -2103,11 +1802,6 @@ msgstr ""
|
||||||
msgid "User not found"
|
msgid "User not found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "You do not have permission to access this custom field value"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "You do not have permission to access this membership fee type"
|
msgid "You do not have permission to access this membership fee type"
|
||||||
|
|
@ -2118,11 +1812,6 @@ msgstr ""
|
||||||
msgid "You do not have permission to access this user"
|
msgid "You do not have permission to access this user"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "You do not have permission to delete this custom field value"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
#: lib/mv_web/live/membership_fee_type_live/index.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "You do not have permission to delete this membership fee type"
|
msgid "You do not have permission to delete this membership fee type"
|
||||||
|
|
@ -2168,11 +1857,6 @@ msgstr ""
|
||||||
msgid "You do not have permission to delete this member"
|
msgid "You do not have permission to delete this member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_value_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
|
||||||
msgid "You do not have permission to view custom field values"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Member created successfully"
|
msgid "Member created successfully"
|
||||||
|
|
@ -2213,12 +1897,324 @@ msgstr ""
|
||||||
msgid "Administration"
|
msgid "Administration"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Failed to %{action} member."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Failed to save member. Please try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Please correct the errors in the form and try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Validation failed. Please check your input."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Validation failed: %{field} %{message}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/form.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Validation failed: %{message}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "Use this form to manage Custom Field Value records in your database."
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Member"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Choose a custom field"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Joining year - reduced to 0"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#~ #: lib/mv_web/components/layouts/sidebar.ex
|
#~ #: lib/mv_web/components/layouts/sidebar.ex
|
||||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
#~ msgid "Admin"
|
#~ msgid "Admin"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Regular"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Current"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Paid via bank transfer"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Mark as Unpaid"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Half-yearly contribution for supporting members"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Reduced fee for unemployed, pensioners, or low income"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "Custom field value not found"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Supporting Member"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Monthly fee for students and trainees"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Custom field value %{action} successfully"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Total Contributions"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Manage contribution types for membership fees."
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Change Contribution Type"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "New Contribution Type"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Time Period"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "Custom field value deleted successfully"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "You do not have permission to access this custom field value"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Cannot delete - members assigned"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Preview Mockup"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Contribution Types"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "This page is not functional and only displays the planned features."
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "Member since"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Unsupported value type: %{type}"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Custom field"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Mark as Paid"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Contribution type"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#~ #: lib/mv_web/components/layouts/sidebar.ex
|
#~ #: lib/mv_web/components/layouts/sidebar.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Contributions"
|
#~ msgid "Contributions"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Reduced"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "No fee for honorary members"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "You do not have permission to delete this custom field value"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "%{count} period selected"
|
||||||
|
#~ msgid_plural "%{count} periods selected"
|
||||||
|
#~ msgstr[0] ""
|
||||||
|
#~ msgstr[1] ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Mark as Suspended"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "Contribution types define different membership fee structures. Each type has a fixed cycle (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Choose a member"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Suspend"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Reopen"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Value"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Why are not all contribution types shown?"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Contribution Start"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Standard membership fee for regular members"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "Save Custom Field Value"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Honorary"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Contributions for %{name}"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Family"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
|
#~ msgid "You do not have permission to view custom field values"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Student"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Quarterly fee for family memberships"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Members can only switch between contribution types with the same payment interval (e.g., yearly to yearly). This prevents complex period overlaps."
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_value_live/form.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Please select a custom field first"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Open Contributions"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_period_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Member Contributions"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/contribution_type_live/index.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "About Contribution Types"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
|
||||||
|
|
@ -158,10 +158,8 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do
|
||||||
|> Ash.update!()
|
|> Ash.update!()
|
||||||
|
|
||||||
# Create a member without explicitly setting membership_fee_type_id
|
# Create a member without explicitly setting membership_fee_type_id
|
||||||
# Note: This test assumes that the Member resource automatically assigns
|
# The Member resource automatically assigns the default_membership_fee_type_id
|
||||||
# the default_membership_fee_type_id during creation. If this is not yet
|
# during creation via SetDefaultMembershipFeeType change.
|
||||||
# implemented, this test will fail initially (which is expected in TDD).
|
|
||||||
# For now, we skip this test as the auto-assignment feature is not yet implemented.
|
|
||||||
{:ok, member} =
|
{:ok, member} =
|
||||||
Ash.create(Member, %{
|
Ash.create(Member, %{
|
||||||
first_name: "Test",
|
first_name: "Test",
|
||||||
|
|
@ -169,10 +167,8 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do
|
||||||
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||||||
})
|
})
|
||||||
|
|
||||||
# TODO: When auto-assignment is implemented, uncomment this assertion
|
# Verify that the default membership fee type was automatically assigned
|
||||||
# assert member.membership_fee_type_id == fee_type.id
|
assert member.membership_fee_type_id == fee_type.id
|
||||||
# For now, we just verify the member was created successfully
|
|
||||||
assert %Member{} = member
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "include_joining_cycle is used during cycle generation" do
|
test "include_joining_cycle is used during cycle generation" do
|
||||||
|
|
|
||||||
|
|
@ -404,7 +404,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
|
||||||
|
|
||||||
assert chunk_result.inserted == 0
|
assert chunk_result.inserted == 0
|
||||||
assert chunk_result.failed == 10
|
assert chunk_result.failed == 10
|
||||||
assert length(chunk_result.errors) == 0
|
assert chunk_result.errors == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "error capping with mixed success and failure" do
|
test "error capping with mixed success and failure" do
|
||||||
|
|
|
||||||
|
|
@ -146,8 +146,6 @@ defmodule MvWeb.ProfileNavigationTest do
|
||||||
"/",
|
"/",
|
||||||
"/members",
|
"/members",
|
||||||
"/members/new",
|
"/members/new",
|
||||||
"/custom_field_values",
|
|
||||||
"/custom_field_values/new",
|
|
||||||
"/users",
|
"/users",
|
||||||
"/users/new"
|
"/users/new"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
274
test/mv_web/live/role_live/show_test.exs
Normal file
274
test/mv_web/live/role_live/show_test.exs
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
defmodule MvWeb.RoleLive.ShowTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for the role show page.
|
||||||
|
|
||||||
|
Tests cover:
|
||||||
|
- Displaying role information
|
||||||
|
- System role badge display
|
||||||
|
- User count display
|
||||||
|
- Navigation
|
||||||
|
- Error handling
|
||||||
|
- Delete functionality
|
||||||
|
"""
|
||||||
|
use MvWeb.ConnCase, async: false
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
require Ash.Query
|
||||||
|
use Gettext, backend: MvWeb.Gettext
|
||||||
|
|
||||||
|
alias Mv.Authorization
|
||||||
|
alias Mv.Authorization.Role
|
||||||
|
|
||||||
|
# Helper to create a role
|
||||||
|
defp create_role(attrs \\ %{}) do
|
||||||
|
default_attrs = %{
|
||||||
|
name: "Test Role #{System.unique_integer([:positive])}",
|
||||||
|
description: "Test description",
|
||||||
|
permission_set_name: "read_only"
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs = Map.merge(default_attrs, attrs)
|
||||||
|
|
||||||
|
case Authorization.create_role(attrs) do
|
||||||
|
{:ok, role} -> role
|
||||||
|
{:error, error} -> raise "Failed to create role: #{inspect(error)}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to create admin user with admin role
|
||||||
|
defp create_admin_user(conn) do
|
||||||
|
# Create admin role
|
||||||
|
admin_role =
|
||||||
|
case Authorization.list_roles() do
|
||||||
|
{:ok, roles} ->
|
||||||
|
case Enum.find(roles, &(&1.name == "Admin")) do
|
||||||
|
nil ->
|
||||||
|
# Create admin role if it doesn't exist
|
||||||
|
create_role(%{
|
||||||
|
name: "Admin",
|
||||||
|
description: "Administrator with full access",
|
||||||
|
permission_set_name: "admin"
|
||||||
|
})
|
||||||
|
|
||||||
|
role ->
|
||||||
|
role
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
# Create admin role if list_roles fails
|
||||||
|
create_role(%{
|
||||||
|
name: "Admin",
|
||||||
|
description: "Administrator with full access",
|
||||||
|
permission_set_name: "admin"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create user
|
||||||
|
{:ok, user} =
|
||||||
|
Mv.Accounts.User
|
||||||
|
|> Ash.Changeset.for_create(:register_with_password, %{
|
||||||
|
email: "admin#{System.unique_integer([:positive])}@mv.local",
|
||||||
|
password: "testpassword123"
|
||||||
|
})
|
||||||
|
|> Ash.create()
|
||||||
|
|
||||||
|
# Assign admin role using manage_relationship
|
||||||
|
{:ok, user} =
|
||||||
|
user
|
||||||
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|
|> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove)
|
||||||
|
|> Ash.update()
|
||||||
|
|
||||||
|
# Load role for authorization checks (must be loaded for can?/3 to work)
|
||||||
|
user_with_role = Ash.load!(user, :role, domain: Mv.Accounts)
|
||||||
|
|
||||||
|
# Store user with role in session for LiveView
|
||||||
|
conn = conn_with_password_user(conn, user_with_role)
|
||||||
|
{conn, user_with_role, admin_role}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "mount and display" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
{conn, _user, _admin_role} = create_admin_user(conn)
|
||||||
|
%{conn: conn}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "mounts successfully with valid role ID", %{conn: conn} do
|
||||||
|
role = create_role()
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, "/admin/roles/#{role.id}")
|
||||||
|
|
||||||
|
assert html =~ role.name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "displays role name", %{conn: conn} do
|
||||||
|
role = create_role(%{name: "Test Role Name"})
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, "/admin/roles/#{role.id}")
|
||||||
|
|
||||||
|
assert html =~ "Test Role Name"
|
||||||
|
assert html =~ gettext("Name")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "displays role description when present", %{conn: conn} do
|
||||||
|
role = create_role(%{description: "This is a test description"})
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, "/admin/roles/#{role.id}")
|
||||||
|
|
||||||
|
assert html =~ "This is a test description"
|
||||||
|
assert html =~ gettext("Description")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "displays 'No description' when description is missing", %{conn: conn} do
|
||||||
|
role = create_role(%{description: nil})
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, "/admin/roles/#{role.id}")
|
||||||
|
|
||||||
|
assert html =~ gettext("No description")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "displays permission set name", %{conn: conn} do
|
||||||
|
role = create_role(%{permission_set_name: "read_only"})
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, "/admin/roles/#{role.id}")
|
||||||
|
|
||||||
|
assert html =~ "read_only"
|
||||||
|
assert html =~ gettext("Permission Set")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "displays system role badge when is_system_role is true", %{conn: conn} do
|
||||||
|
system_role =
|
||||||
|
Role
|
||||||
|
|> Ash.Changeset.for_create(:create_role, %{
|
||||||
|
name: "System Role",
|
||||||
|
permission_set_name: "own_data"
|
||||||
|
})
|
||||||
|
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||||
|
|> Ash.create!()
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, "/admin/roles/#{system_role.id}")
|
||||||
|
|
||||||
|
assert html =~ gettext("System Role")
|
||||||
|
assert html =~ gettext("Yes")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "displays non-system role badge when is_system_role is false", %{conn: conn} do
|
||||||
|
role = create_role()
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, "/admin/roles/#{role.id}")
|
||||||
|
|
||||||
|
assert html =~ gettext("System Role")
|
||||||
|
assert html =~ gettext("No")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "displays user count", %{conn: conn} do
|
||||||
|
role = create_role()
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, "/admin/roles/#{role.id}")
|
||||||
|
|
||||||
|
# User count should be displayed (might be 0 or more)
|
||||||
|
assert html =~ gettext("User") || html =~ "0" || html =~ "users"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "navigation" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
{conn, _user, _admin_role} = create_admin_user(conn)
|
||||||
|
%{conn: conn}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "back button navigates to role list", %{conn: conn} do
|
||||||
|
role = create_role()
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, "/admin/roles/#{role.id}")
|
||||||
|
|
||||||
|
assert {:error, {:live_redirect, %{to: to}}} =
|
||||||
|
view
|
||||||
|
|> element(
|
||||||
|
"a[aria-label='#{gettext("Back to roles list")}'], button[aria-label='#{gettext("Back to roles list")}']"
|
||||||
|
)
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
assert to == "/admin/roles"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "edit button navigates to edit form", %{conn: conn} do
|
||||||
|
role = create_role()
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, "/admin/roles/#{role.id}")
|
||||||
|
|
||||||
|
assert {:error, {:live_redirect, %{to: to}}} =
|
||||||
|
view
|
||||||
|
|> element(
|
||||||
|
"a[href='/admin/roles/#{role.id}/edit'], button[href='/admin/roles/#{role.id}/edit']"
|
||||||
|
)
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
assert to == "/admin/roles/#{role.id}/edit"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "error handling" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
{conn, _user, _admin_role} = create_admin_user(conn)
|
||||||
|
%{conn: conn}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects to role list with error for invalid role ID", %{conn: conn} do
|
||||||
|
invalid_id = Ecto.UUID.generate()
|
||||||
|
|
||||||
|
# Should redirect to index with error message
|
||||||
|
result = live(conn, "/admin/roles/#{invalid_id}")
|
||||||
|
|
||||||
|
assert match?({:error, {:redirect, %{to: "/admin/roles"}}}, result) or
|
||||||
|
match?({:error, {:live_redirect, %{to: "/admin/roles"}}}, result)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "delete functionality" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
{conn, _user, _admin_role} = create_admin_user(conn)
|
||||||
|
%{conn: conn}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "delete button is not shown for system roles", %{conn: conn} do
|
||||||
|
system_role =
|
||||||
|
Role
|
||||||
|
|> Ash.Changeset.for_create(:create_role, %{
|
||||||
|
name: "System Role",
|
||||||
|
permission_set_name: "own_data"
|
||||||
|
})
|
||||||
|
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|
||||||
|
|> Ash.create!()
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, "/admin/roles/#{system_role.id}")
|
||||||
|
|
||||||
|
# Delete button should not be visible for system roles
|
||||||
|
refute html =~ ~r/Delete.*Role.*#{system_role.id}/i
|
||||||
|
end
|
||||||
|
|
||||||
|
test "delete button is shown for non-system roles", %{conn: conn} do
|
||||||
|
role = create_role()
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, "/admin/roles/#{role.id}")
|
||||||
|
|
||||||
|
# Delete button should be visible for non-system roles
|
||||||
|
assert html =~ gettext("Delete Role") || html =~ "delete"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "page title" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
{conn, _user, _admin_role} = create_admin_user(conn)
|
||||||
|
%{conn: conn}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sets correct page title", %{conn: conn} do
|
||||||
|
role = create_role()
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, "/admin/roles/#{role.id}")
|
||||||
|
|
||||||
|
# Check that page title is set (might be in title tag or header)
|
||||||
|
assert html =~ gettext("Show Role") || html =~ role.name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
155
test/mv_web/live/user_live/show_test.exs
Normal file
155
test/mv_web/live/user_live/show_test.exs
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
defmodule MvWeb.UserLive.ShowTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for the user show page.
|
||||||
|
|
||||||
|
Tests cover:
|
||||||
|
- Displaying user information
|
||||||
|
- Authentication status display
|
||||||
|
- Linked member display
|
||||||
|
- Navigation
|
||||||
|
- Error handling
|
||||||
|
"""
|
||||||
|
use MvWeb.ConnCase, async: true
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
require Ash.Query
|
||||||
|
use Gettext, backend: MvWeb.Gettext
|
||||||
|
|
||||||
|
alias Mv.Membership.Member
|
||||||
|
|
||||||
|
setup do
|
||||||
|
# Create test user
|
||||||
|
user = create_test_user(%{email: "test@example.com", oidc_id: "test123"})
|
||||||
|
%{user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "mount and display" do
|
||||||
|
test "mounts successfully with valid user ID", %{conn: conn, user: user} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/users/#{user.id}")
|
||||||
|
|
||||||
|
assert html =~ to_string(user.email)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "displays user email", %{conn: conn, user: user} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/users/#{user.id}")
|
||||||
|
|
||||||
|
assert html =~ to_string(user.email)
|
||||||
|
assert html =~ gettext("Email")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "displays password authentication status when enabled", %{conn: conn} do
|
||||||
|
user = create_test_user(%{email: "password-user@example.com", password: "test123"})
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/users/#{user.id}")
|
||||||
|
|
||||||
|
assert html =~ gettext("Password Authentication")
|
||||||
|
assert html =~ gettext("Enabled")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "displays password authentication status when not enabled", %{conn: conn} do
|
||||||
|
# User without password (only OIDC) - create user with OIDC only
|
||||||
|
user =
|
||||||
|
create_test_user(%{
|
||||||
|
email: "oidc-only#{System.unique_integer([:positive])}@example.com",
|
||||||
|
oidc_id: "oidc#{System.unique_integer([:positive])}",
|
||||||
|
hashed_password: nil
|
||||||
|
})
|
||||||
|
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/users/#{user.id}")
|
||||||
|
|
||||||
|
assert html =~ gettext("Password Authentication")
|
||||||
|
assert html =~ gettext("Not enabled")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "displays linked member when present", %{conn: conn} do
|
||||||
|
# Create member
|
||||||
|
{:ok, member} =
|
||||||
|
Member
|
||||||
|
|> Ash.Changeset.for_create(:create_member, %{
|
||||||
|
first_name: "Alice",
|
||||||
|
last_name: "Smith",
|
||||||
|
email: "alice@example.com"
|
||||||
|
})
|
||||||
|
|> Ash.create()
|
||||||
|
|
||||||
|
# Create user and link to member
|
||||||
|
user = create_test_user(%{email: "user@example.com"})
|
||||||
|
|
||||||
|
{:ok, _updated_user} =
|
||||||
|
user
|
||||||
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|
|> Ash.Changeset.manage_relationship(:member, member, type: :append_and_remove)
|
||||||
|
|> Ash.update()
|
||||||
|
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/users/#{user.id}")
|
||||||
|
|
||||||
|
assert html =~ gettext("Linked Member")
|
||||||
|
assert html =~ "Alice Smith"
|
||||||
|
assert html =~ ~r/href="[^"]*\/members\/#{member.id}"/
|
||||||
|
end
|
||||||
|
|
||||||
|
test "displays 'No member linked' when no member is linked", %{conn: conn, user: user} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/users/#{user.id}")
|
||||||
|
|
||||||
|
assert html =~ gettext("Linked Member")
|
||||||
|
assert html =~ gettext("No member linked")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "navigation" do
|
||||||
|
test "back button navigates to user list", %{conn: conn, user: user} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/users/#{user.id}")
|
||||||
|
|
||||||
|
assert {:error, {:live_redirect, %{to: to}}} =
|
||||||
|
view
|
||||||
|
|> element(
|
||||||
|
"a[aria-label='#{gettext("Back to users list")}'], button[aria-label='#{gettext("Back to users list")}']"
|
||||||
|
)
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
assert to == "/users"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "edit button navigates to edit form", %{conn: conn, user: user} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/users/#{user.id}")
|
||||||
|
|
||||||
|
assert {:error, {:live_redirect, %{to: to}}} =
|
||||||
|
view
|
||||||
|
|> element(
|
||||||
|
"a[href='/users/#{user.id}/edit?return_to=show'], button[href='/users/#{user.id}/edit?return_to=show']"
|
||||||
|
)
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
assert to == "/users/#{user.id}/edit?return_to=show"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "error handling" do
|
||||||
|
test "raises exception for invalid user ID", %{conn: conn} do
|
||||||
|
invalid_id = Ecto.UUID.generate()
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
|
||||||
|
# The mount function uses Ash.get! which will raise an exception
|
||||||
|
# This is expected behavior - the LiveView doesn't handle this case
|
||||||
|
assert_raise Ash.Error.Invalid, fn ->
|
||||||
|
live(conn, ~p"/users/#{invalid_id}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "page title" do
|
||||||
|
test "sets correct page title", %{conn: conn, user: user} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/users/#{user.id}")
|
||||||
|
|
||||||
|
# Check that page title is set (might be in title tag or header)
|
||||||
|
assert html =~ gettext("Show User") || html =~ to_string(user.email)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Add a link
Reference in a new issue