Compare commits
10 commits
6ba7c5bdbc
...
49184d2631
| Author | SHA1 | Date | |
|---|---|---|---|
| 49184d2631 | |||
| 75fe26fad8 | |||
| fcf7cbc558 | |||
| ac2ad0a0d5 | |||
| 875c422b7d | |||
| 6d75766dba | |||
| 354029c9cc | |||
| 671e6ce804 | |||
| 386b4c9e65 | |||
| c8968636a8 |
26 changed files with 4455 additions and 251 deletions
653
docs/contributions-architecture.md
Normal file
653
docs/contributions-architecture.md
Normal file
|
|
@ -0,0 +1,653 @@
|
||||||
|
# Membership Contributions - Technical Architecture
|
||||||
|
|
||||||
|
**Project:** Mila - Membership Management System
|
||||||
|
**Feature:** Membership Contribution Management
|
||||||
|
**Version:** 1.0
|
||||||
|
**Last Updated:** 2025-11-27
|
||||||
|
**Status:** Architecture Design - Ready for Implementation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document defines the technical architecture for the Membership Contributions system. It focuses on architectural decisions, patterns, module structure, and integration points **without** concrete implementation details.
|
||||||
|
|
||||||
|
**Related Documents:**
|
||||||
|
- [contributions-overview.md](./contributions-overview.md) - Business logic and requirements
|
||||||
|
- [database-schema-readme.md](./database-schema-readme.md) - Database documentation
|
||||||
|
- [database_schema.dbml](./database_schema.dbml) - Database schema definition
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Architecture Principles](#architecture-principles)
|
||||||
|
2. [Domain Structure](#domain-structure)
|
||||||
|
3. [Data Architecture](#data-architecture)
|
||||||
|
4. [Business Logic Architecture](#business-logic-architecture)
|
||||||
|
5. [Integration Points](#integration-points)
|
||||||
|
6. [Acceptance Criteria](#acceptance-criteria)
|
||||||
|
7. [Testing Strategy](#testing-strategy)
|
||||||
|
8. [Security Considerations](#security-considerations)
|
||||||
|
9. [Performance Considerations](#performance-considerations)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Principles
|
||||||
|
|
||||||
|
### Core Design Decisions
|
||||||
|
|
||||||
|
1. **Single Responsibility:**
|
||||||
|
- Each module has one clear responsibility
|
||||||
|
- Period generation separated from status management
|
||||||
|
- Calendar logic isolated in dedicated module
|
||||||
|
|
||||||
|
2. **No Redundancy:**
|
||||||
|
- No `period_end` field (calculated from `period_start` + `interval`)
|
||||||
|
- No `interval_type` field (read from `contribution_type.interval`)
|
||||||
|
- Eliminates data inconsistencies
|
||||||
|
|
||||||
|
3. **Immutability Where Important:**
|
||||||
|
- `contribution_type.interval` cannot be changed after creation
|
||||||
|
- Prevents complex migration scenarios
|
||||||
|
- Enforced via Ash change validation
|
||||||
|
|
||||||
|
4. **Historical Accuracy:**
|
||||||
|
- `amount` stored per period for audit trail
|
||||||
|
- Enables tracking of contribution changes over time
|
||||||
|
- Old periods retain original amounts
|
||||||
|
|
||||||
|
5. **Calendar-Based Periods:**
|
||||||
|
- All periods aligned to calendar boundaries
|
||||||
|
- Simplifies date calculations
|
||||||
|
- Predictable period generation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Domain Structure
|
||||||
|
|
||||||
|
### Ash Domain: `Mv.Contributions`
|
||||||
|
|
||||||
|
**Purpose:** Encapsulates all contribution-related resources and logic
|
||||||
|
|
||||||
|
**Resources:**
|
||||||
|
- `ContributionType` - Contribution type definitions (admin-managed)
|
||||||
|
- `ContributionPeriod` - Individual contribution periods per member
|
||||||
|
|
||||||
|
**Extensions:**
|
||||||
|
- Member resource extended with contribution fields
|
||||||
|
|
||||||
|
### Module Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── contributions/
|
||||||
|
│ ├── contributions.ex # Ash domain definition
|
||||||
|
│ ├── contribution_type.ex # ContributionType resource
|
||||||
|
│ ├── contribution_period.ex # ContributionPeriod resource
|
||||||
|
│ └── changes/
|
||||||
|
│ ├── prevent_interval_change.ex # Validates interval immutability
|
||||||
|
│ ├── set_contribution_start_date.ex # Auto-sets start date
|
||||||
|
│ └── validate_same_interval.ex # Validates interval match on type change
|
||||||
|
├── mv/
|
||||||
|
│ └── contributions/
|
||||||
|
│ ├── period_generator.ex # Period generation algorithm
|
||||||
|
│ └── calendar_periods.ex # Calendar period calculations
|
||||||
|
└── membership/
|
||||||
|
└── member.ex # Extended with contribution relationships
|
||||||
|
```
|
||||||
|
|
||||||
|
### Separation of Concerns
|
||||||
|
|
||||||
|
**Domain Layer (Ash Resources):**
|
||||||
|
- Data validation
|
||||||
|
- Relationship management
|
||||||
|
- Policy enforcement
|
||||||
|
- Action definitions
|
||||||
|
|
||||||
|
**Business Logic Layer (`Mv.Contributions`):**
|
||||||
|
- Period generation algorithm
|
||||||
|
- Calendar calculations
|
||||||
|
- Date boundary handling
|
||||||
|
- Status transitions
|
||||||
|
|
||||||
|
**UI Layer (LiveView):**
|
||||||
|
- User interaction
|
||||||
|
- Display logic
|
||||||
|
- Authorization checks
|
||||||
|
- Form handling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Architecture
|
||||||
|
|
||||||
|
### Database Schema Extensions
|
||||||
|
|
||||||
|
**See:** [database-schema-readme.md](./database-schema-readme.md) and [database_schema.dbml](./database_schema.dbml) for complete schema documentation.
|
||||||
|
|
||||||
|
### New Tables
|
||||||
|
|
||||||
|
1. **`contribution_types`**
|
||||||
|
- Purpose: Define contribution types with fixed intervals
|
||||||
|
- Key Constraint: `interval` field immutable after creation
|
||||||
|
- Relationships: has_many members, has_many contribution_periods
|
||||||
|
|
||||||
|
2. **`contribution_periods`**
|
||||||
|
- Purpose: Individual contribution periods for members
|
||||||
|
- Key Design: NO `period_end` or `interval_type` fields (calculated)
|
||||||
|
- Relationships: belongs_to member, belongs_to contribution_type
|
||||||
|
- Composite uniqueness: One period per member per period_start
|
||||||
|
|
||||||
|
### Member Table Extensions
|
||||||
|
|
||||||
|
**Fields Added:**
|
||||||
|
- `contribution_type_id` (FK, NOT NULL with default from settings)
|
||||||
|
- `contribution_start_date` (Date, nullable)
|
||||||
|
|
||||||
|
**Existing Fields Used:**
|
||||||
|
- `joined_at` - For calculating contribution start
|
||||||
|
- `left_at` - For limiting period generation
|
||||||
|
- These fields must remain member fields and should not be replaced by custom fields in the future
|
||||||
|
|
||||||
|
### Settings Integration
|
||||||
|
|
||||||
|
**Global Settings:**
|
||||||
|
- `contributions.include_joining_period` (Boolean)
|
||||||
|
- `contributions.default_contribution_type_id` (UUID)
|
||||||
|
|
||||||
|
**Storage:** Existing settings mechanism (TBD: dedicated table or configuration resource)
|
||||||
|
|
||||||
|
### Foreign Key Behaviors
|
||||||
|
|
||||||
|
| Relationship | On Delete | Rationale |
|
||||||
|
|--------------|-----------|-----------|
|
||||||
|
| `contribution_periods.member_id → members.id` | CASCADE | Remove periods when member deleted |
|
||||||
|
| `contribution_periods.contribution_type_id → contribution_types.id` | RESTRICT | Prevent type deletion if periods exist |
|
||||||
|
| `members.contribution_type_id → contribution_types.id` | RESTRICT | Prevent type deletion if assigned to members |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business Logic Architecture
|
||||||
|
|
||||||
|
### Period Generation System
|
||||||
|
|
||||||
|
**Component:** `Mv.Contributions.PeriodGenerator`
|
||||||
|
|
||||||
|
**Responsibilities:**
|
||||||
|
- Calculate which periods should exist for a member
|
||||||
|
- Generate missing periods
|
||||||
|
- Respect contribution_start_date and left_at boundaries
|
||||||
|
- Skip existing periods (idempotent)
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
1. Member contribution type assigned (via Ash change)
|
||||||
|
2. Member created with contribution type (via Ash change)
|
||||||
|
3. Scheduled job runs (daily/weekly cron)
|
||||||
|
4. Admin manual regeneration (UI action)
|
||||||
|
|
||||||
|
**Algorithm Steps:**
|
||||||
|
1. Retrieve member with contribution_type and dates
|
||||||
|
2. Determine first period start (based on contribution_start_date)
|
||||||
|
3. Calculate all period starts from first to today (or left_at)
|
||||||
|
4. Query existing periods for member
|
||||||
|
5. Generate missing periods with current contribution_type.amount
|
||||||
|
6. Insert new periods (batch operation)
|
||||||
|
|
||||||
|
**Edge Case Handling:**
|
||||||
|
- If contribution_start_date is NULL: Calculate from joined_at + global setting
|
||||||
|
- If left_at is set: Stop generation at left_at
|
||||||
|
- If contribution_type changes: Handled separately by regeneration logic
|
||||||
|
|
||||||
|
### Calendar Period Calculations
|
||||||
|
|
||||||
|
**Component:** `Mv.Contributions.CalendarPeriods`
|
||||||
|
|
||||||
|
**Responsibilities:**
|
||||||
|
- Calculate period boundaries based on interval type
|
||||||
|
- Determine current period
|
||||||
|
- Determine last completed period
|
||||||
|
- Calculate period_end from period_start + interval
|
||||||
|
|
||||||
|
**Functions (high-level):**
|
||||||
|
- `calculate_period_start/3` - Given date and interval, find period start
|
||||||
|
- `calculate_period_end/2` - Given period_start and interval, calculate end
|
||||||
|
- `next_period_start/2` - Given period_start and interval, find next
|
||||||
|
- `is_current_period?/2` - Check if period contains today
|
||||||
|
- `is_last_completed_period?/2` - Check if period just ended
|
||||||
|
|
||||||
|
**Interval Logic:**
|
||||||
|
- **Monthly:** Start = 1st of month, End = last day of month
|
||||||
|
- **Quarterly:** Start = 1st of quarter (Jan/Apr/Jul/Oct), End = last day of quarter
|
||||||
|
- **Half-yearly:** Start = 1st of half (Jan/Jul), End = last day of half
|
||||||
|
- **Yearly:** Start = Jan 1st, End = Dec 31st
|
||||||
|
|
||||||
|
### Status Management
|
||||||
|
|
||||||
|
**Component:** Ash actions on `ContributionPeriod`
|
||||||
|
|
||||||
|
**Status Transitions:**
|
||||||
|
- Simple state machine: unpaid ↔ paid ↔ suspended
|
||||||
|
- No complex validation (all transitions allowed)
|
||||||
|
- Permissions checked via Ash policies
|
||||||
|
|
||||||
|
**Actions Required:**
|
||||||
|
- `mark_as_paid` - Set status to :paid
|
||||||
|
- `mark_as_suspended` - Set status to :suspended
|
||||||
|
- `mark_as_unpaid` - Set status to :unpaid (error correction)
|
||||||
|
|
||||||
|
**Bulk Operations:**
|
||||||
|
- `bulk_mark_as_paid` - Mark multiple periods as paid (efficiency)
|
||||||
|
- low priority, can be a future issue
|
||||||
|
|
||||||
|
### Contribution Type Change Handling
|
||||||
|
|
||||||
|
**Component:** Ash change on `Member.contribution_type_id`
|
||||||
|
|
||||||
|
**Validation:**
|
||||||
|
- Check if new type has same interval as old type
|
||||||
|
- If different: Reject change (MVP constraint)
|
||||||
|
- If same: Allow change
|
||||||
|
|
||||||
|
**Side Effects on Allowed Change:**
|
||||||
|
1. Keep all existing periods unchanged
|
||||||
|
2. Find future unpaid periods
|
||||||
|
3. Delete future unpaid periods
|
||||||
|
4. Regenerate periods with new contribution_type_id and amount
|
||||||
|
|
||||||
|
**Implementation Pattern:**
|
||||||
|
- Use Ash change module to validate
|
||||||
|
- Use after_action hook to trigger regeneration
|
||||||
|
- Use transaction to ensure atomicity
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration Points
|
||||||
|
|
||||||
|
### Member Resource Integration
|
||||||
|
|
||||||
|
**Extension Points:**
|
||||||
|
1. Add fields via migration
|
||||||
|
2. Add relationships (belongs_to, has_many)
|
||||||
|
3. Add calculations (current_period_status, overdue_count)
|
||||||
|
4. Add changes (auto-set contribution_start_date, validate interval)
|
||||||
|
|
||||||
|
**Backward Compatibility:**
|
||||||
|
- New fields nullable or with defaults
|
||||||
|
- Existing members get default contribution type from settings
|
||||||
|
- No breaking changes to existing member functionality
|
||||||
|
|
||||||
|
### Settings System Integration
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
- Store two global settings
|
||||||
|
- Provide UI for admin to modify
|
||||||
|
- Default values if not set
|
||||||
|
- Validation (e.g., default_contribution_type_id must exist)
|
||||||
|
|
||||||
|
**Access Pattern:**
|
||||||
|
- Read settings during period generation
|
||||||
|
- Read settings during member creation
|
||||||
|
- Write settings only via admin UI
|
||||||
|
|
||||||
|
### Permission System Integration
|
||||||
|
|
||||||
|
**See:** [roles-and-permissions-architecture.md](./roles-and-permissions-architecture.md)
|
||||||
|
|
||||||
|
**Required Permissions:**
|
||||||
|
- `ContributionType.create/update/destroy` - Admin only
|
||||||
|
- `ContributionType.read` - Admin, Treasurer, Board
|
||||||
|
- `ContributionPeriod.update` (status changes) - Admin, Treasurer
|
||||||
|
- `ContributionPeriod.read` - Admin, Treasurer, Board, Own member
|
||||||
|
|
||||||
|
**Policy Patterns:**
|
||||||
|
- Use existing HasPermission check
|
||||||
|
- Leverage existing roles (Admin, Kassenwart)
|
||||||
|
- Member can read own periods (linked via member_id)
|
||||||
|
|
||||||
|
### LiveView Integration
|
||||||
|
|
||||||
|
**New LiveViews Required:**
|
||||||
|
1. ContributionType index/form (admin)
|
||||||
|
2. ContributionPeriod table component (member detail view)
|
||||||
|
3. Settings form section (admin)
|
||||||
|
4. Member list column (contribution status)
|
||||||
|
|
||||||
|
**Existing LiveViews to Extend:**
|
||||||
|
- Member detail view: Add contributions section
|
||||||
|
- Member list view: Add status column
|
||||||
|
- Settings page: Add contributions section
|
||||||
|
|
||||||
|
**Authorization Helpers:**
|
||||||
|
- Use existing `can?/3` helper for UI conditionals
|
||||||
|
- Check permissions before showing actions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
### ContributionType Resource
|
||||||
|
|
||||||
|
**AC-CT-1:** Admin can create contribution type with name, amount, interval, description
|
||||||
|
**AC-CT-2:** Interval field is immutable after creation (validation error on change attempt)
|
||||||
|
**AC-CT-3:** Admin can update name, amount, description (but not interval)
|
||||||
|
**AC-CT-4:** Cannot delete contribution type if assigned to members
|
||||||
|
**AC-CT-5:** Cannot delete contribution type if periods exist referencing it
|
||||||
|
**AC-CT-6:** Interval must be one of: monthly, quarterly, half_yearly, yearly
|
||||||
|
|
||||||
|
### ContributionPeriod Resource
|
||||||
|
|
||||||
|
**AC-CP-1:** Period has period_start, status, amount, notes, member_id, contribution_type_id
|
||||||
|
**AC-CP-2:** Period_end is calculated, not stored
|
||||||
|
**AC-CP-3:** Status defaults to :unpaid
|
||||||
|
**AC-CP-4:** One period per member per period_start (uniqueness constraint)
|
||||||
|
**AC-CP-5:** Amount is set at generation time from contribution_type.amount
|
||||||
|
**AC-CP-6:** Periods cascade delete when member deleted
|
||||||
|
**AC-CP-7:** Admin/Treasurer can change status
|
||||||
|
**AC-CP-8:** Member can read own periods
|
||||||
|
|
||||||
|
### Member Extensions
|
||||||
|
|
||||||
|
**AC-M-1:** Member has contribution_type_id field (NOT NULL with default)
|
||||||
|
**AC-M-2:** Member has contribution_start_date field (nullable)
|
||||||
|
**AC-M-3:** New members get default contribution type from global setting
|
||||||
|
**AC-M-4:** contribution_start_date auto-set based on joined_at and global setting
|
||||||
|
**AC-M-5:** Admin can manually override contribution_start_date
|
||||||
|
**AC-M-6:** Cannot change to contribution type with different interval (MVP)
|
||||||
|
|
||||||
|
### Period Generation
|
||||||
|
|
||||||
|
**AC-PG-1:** Periods generated when member gets contribution type
|
||||||
|
**AC-PG-2:** Periods generated when member created (via change hook)
|
||||||
|
**AC-PG-3:** Scheduled job generates missing periods daily
|
||||||
|
**AC-PG-4:** Generation respects contribution_start_date
|
||||||
|
**AC-PG-5:** Generation stops at left_at if member exited
|
||||||
|
**AC-PG-6:** Generation is idempotent (skips existing periods)
|
||||||
|
**AC-PG-7:** Periods align to calendar boundaries (1st of month/quarter/half/year)
|
||||||
|
**AC-PG-8:** Amount comes from contribution_type at generation time
|
||||||
|
|
||||||
|
### Calendar Logic
|
||||||
|
|
||||||
|
**AC-CL-1:** Monthly periods: 1st to last day of month
|
||||||
|
**AC-CL-2:** Quarterly periods: 1st of Jan/Apr/Jul/Oct to last day of quarter
|
||||||
|
**AC-CL-3:** Half-yearly periods: 1st of Jan/Jul to last day of half
|
||||||
|
**AC-CL-4:** Yearly periods: Jan 1 to Dec 31
|
||||||
|
**AC-CL-5:** Period_end calculated correctly for all interval types
|
||||||
|
**AC-CL-6:** Current period determined correctly based on today's date
|
||||||
|
**AC-CL-7:** Last completed period determined correctly
|
||||||
|
|
||||||
|
### Contribution Type Change
|
||||||
|
|
||||||
|
**AC-TC-1:** Can change to type with same interval
|
||||||
|
**AC-TC-2:** Cannot change to type with different interval (error message)
|
||||||
|
**AC-TC-3:** On allowed change: future unpaid periods regenerated
|
||||||
|
**AC-TC-4:** On allowed change: paid/suspended periods unchanged
|
||||||
|
**AC-TC-5:** On allowed change: amount updated to new type's amount
|
||||||
|
**AC-TC-6:** Change is atomic (transaction)
|
||||||
|
|
||||||
|
### Settings
|
||||||
|
|
||||||
|
**AC-S-1:** Global setting: include_joining_period (boolean, default true)
|
||||||
|
**AC-S-2:** Global setting: default_contribution_type_id (UUID, required)
|
||||||
|
**AC-S-3:** Admin can modify settings via UI
|
||||||
|
**AC-S-4:** Settings validated (e.g., default type must exist)
|
||||||
|
**AC-S-5:** Settings applied to new members immediately
|
||||||
|
|
||||||
|
### UI - Member List
|
||||||
|
|
||||||
|
**AC-UI-ML-1:** New column shows contribution status
|
||||||
|
**AC-UI-ML-2:** Default: Shows last completed period status
|
||||||
|
**AC-UI-ML-3:** Optional: Toggle to show current period status
|
||||||
|
**AC-UI-ML-4:** Color coding: green (paid), red (unpaid), gray (suspended)
|
||||||
|
**AC-UI-ML-5:** Filter: Unpaid in last period
|
||||||
|
**AC-UI-ML-6:** Filter: Unpaid in current period
|
||||||
|
|
||||||
|
### UI - Member Detail
|
||||||
|
|
||||||
|
**AC-UI-MD-1:** Contributions section shows all periods
|
||||||
|
**AC-UI-MD-2:** Table columns: Period, Interval, Amount, Status, Actions
|
||||||
|
**AC-UI-MD-3:** Checkbox per period for bulk marking (low prio)
|
||||||
|
**AC-UI-MD-4:** "Mark selected as paid" button
|
||||||
|
**AC-UI-MD-5:** Dropdown to change contribution type (same interval only)
|
||||||
|
**AC-UI-MD-6:** Warning if different interval selected
|
||||||
|
**AC-UI-MD-7:** Only show actions if user has permission
|
||||||
|
|
||||||
|
### UI - Contribution Types Admin
|
||||||
|
|
||||||
|
**AC-UI-CTA-1:** List all contribution types
|
||||||
|
**AC-UI-CTA-2:** Show: Name, Amount, Interval, Member count
|
||||||
|
**AC-UI-CTA-3:** Create new contribution type form
|
||||||
|
**AC-UI-CTA-4:** Edit form: Name, Amount, Description editable
|
||||||
|
**AC-UI-CTA-5:** Edit form: Interval grayed out (not editable)
|
||||||
|
**AC-UI-CTA-6:** Warning on amount change (explain impact)
|
||||||
|
**AC-UI-CTA-7:** Cannot delete if members assigned
|
||||||
|
**AC-UI-CTA-8:** Only admin can access
|
||||||
|
|
||||||
|
### UI - Settings Admin
|
||||||
|
|
||||||
|
**AC-UI-SA-1:** Contributions section in settings
|
||||||
|
**AC-UI-SA-2:** Dropdown to select default contribution type
|
||||||
|
**AC-UI-SA-3:** Checkbox: Include joining period
|
||||||
|
**AC-UI-SA-4:** Explanatory text with examples
|
||||||
|
**AC-UI-SA-5:** Save button with validation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Unit Testing
|
||||||
|
|
||||||
|
**Period Generator Tests:**
|
||||||
|
- Correct period_start calculation for all interval types
|
||||||
|
- Correct period count from start to end date
|
||||||
|
- Respects contribution_start_date boundary
|
||||||
|
- Respects left_at boundary
|
||||||
|
- Skips existing periods (idempotent)
|
||||||
|
- Handles edge dates (year boundaries, leap years)
|
||||||
|
|
||||||
|
**Calendar Periods Tests:**
|
||||||
|
- Period boundaries correct for all intervals
|
||||||
|
- Period_end calculation correct
|
||||||
|
- Current period detection
|
||||||
|
- Last completed period detection
|
||||||
|
- Next period calculation
|
||||||
|
|
||||||
|
**Validation Tests:**
|
||||||
|
- Interval immutability enforced
|
||||||
|
- Same interval validation on type change
|
||||||
|
- Status transitions allowed
|
||||||
|
- Uniqueness constraints enforced
|
||||||
|
|
||||||
|
### Integration Testing
|
||||||
|
|
||||||
|
**Period Generation Flow:**
|
||||||
|
- Member creation triggers generation
|
||||||
|
- Type assignment triggers generation
|
||||||
|
- Type change regenerates future periods
|
||||||
|
- Scheduled job generates missing periods
|
||||||
|
- Left member stops generation
|
||||||
|
|
||||||
|
**Status Management Flow:**
|
||||||
|
- Mark single period as paid
|
||||||
|
- Bulk mark multiple periods (low prio)
|
||||||
|
- Status transitions work
|
||||||
|
- Permissions enforced
|
||||||
|
|
||||||
|
**Contribution Type Management:**
|
||||||
|
- Create type
|
||||||
|
- Update amount (regeneration triggered)
|
||||||
|
- Cannot update interval
|
||||||
|
- Cannot delete if in use
|
||||||
|
|
||||||
|
### LiveView Testing
|
||||||
|
|
||||||
|
**Member List:**
|
||||||
|
- Status column displays correctly
|
||||||
|
- Toggle between last/current works
|
||||||
|
- Filters work correctly
|
||||||
|
- Color coding applied
|
||||||
|
|
||||||
|
**Member Detail:**
|
||||||
|
- Periods table displays all periods
|
||||||
|
- Checkboxes work
|
||||||
|
- Bulk marking works (low prio)
|
||||||
|
- Type change validation works
|
||||||
|
- Actions only shown with permission
|
||||||
|
|
||||||
|
**Admin UI:**
|
||||||
|
- Type CRUD works
|
||||||
|
- Settings save correctly
|
||||||
|
- Validations display errors
|
||||||
|
- Only authorized users can access
|
||||||
|
|
||||||
|
### Edge Case Testing
|
||||||
|
|
||||||
|
**Interval Change Attempt:**
|
||||||
|
- Error message displayed
|
||||||
|
- No data modified
|
||||||
|
- User can cancel/choose different type
|
||||||
|
|
||||||
|
**Exit with Unpaid:**
|
||||||
|
- Warning shown
|
||||||
|
- Option to suspend offered
|
||||||
|
- Exit completes correctly
|
||||||
|
|
||||||
|
**Amount Change:**
|
||||||
|
- Warning displayed
|
||||||
|
- Only future unpaid regenerated
|
||||||
|
- Historical periods unchanged
|
||||||
|
|
||||||
|
**Date Boundaries:**
|
||||||
|
- Today = period start handled
|
||||||
|
- Today = period end handled
|
||||||
|
- Leap year handled
|
||||||
|
|
||||||
|
### Performance Testing
|
||||||
|
|
||||||
|
**Period Generation:**
|
||||||
|
- Generate 10 years of monthly periods: < 100ms
|
||||||
|
- Generate for 1000 members: < 5 seconds
|
||||||
|
- Idempotent check efficient (no full scan)
|
||||||
|
|
||||||
|
**Member List Query:**
|
||||||
|
- With status column: < 200ms for 1000 members
|
||||||
|
- Filters applied efficiently
|
||||||
|
- No N+1 queries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
**Permissions Required:**
|
||||||
|
- ContributionType management: Admin only
|
||||||
|
- ContributionPeriod status changes: Admin + Treasurer
|
||||||
|
- View all periods: Admin + Treasurer + Board
|
||||||
|
- View own periods: All authenticated users
|
||||||
|
|
||||||
|
**Policy Enforcement:**
|
||||||
|
- All actions protected by Ash policies
|
||||||
|
- UI shows/hides based on permissions
|
||||||
|
- Backend validates permissions (never trust UI alone)
|
||||||
|
|
||||||
|
### Data Integrity
|
||||||
|
|
||||||
|
**Validation Layers:**
|
||||||
|
1. Database constraints (NOT NULL, UNIQUE, CHECK)
|
||||||
|
2. Ash validations (business rules)
|
||||||
|
3. UI validations (user experience)
|
||||||
|
|
||||||
|
**Immutability Protection:**
|
||||||
|
- Interval change prevented at multiple layers
|
||||||
|
- Period amounts immutable (audit trail)
|
||||||
|
- Settings changes logged (future)
|
||||||
|
|
||||||
|
### Audit Trail
|
||||||
|
|
||||||
|
**Tracked Information:**
|
||||||
|
- Period status changes (who, when) - future enhancement
|
||||||
|
- Type amount changes (implicit via period amounts)
|
||||||
|
- Member type assignments (via timestamps)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Database Indexes
|
||||||
|
|
||||||
|
**Required Indexes:**
|
||||||
|
- `contribution_periods(member_id)` - For member period lookups
|
||||||
|
- `contribution_periods(contribution_type_id)` - For type queries
|
||||||
|
- `contribution_periods(status)` - For unpaid filters
|
||||||
|
- `contribution_periods(period_start)` - For date range queries
|
||||||
|
- `contribution_periods(member_id, period_start)` - Composite unique index
|
||||||
|
- `members(contribution_type_id)` - For type membership count
|
||||||
|
|
||||||
|
### Query Optimization
|
||||||
|
|
||||||
|
**Preloading:**
|
||||||
|
- Load contribution_type with periods (avoid N+1)
|
||||||
|
- Load periods when displaying member detail
|
||||||
|
- Use Ash's load for efficient preloading
|
||||||
|
|
||||||
|
**Calculated Fields:**
|
||||||
|
- period_end calculated on-demand (not stored)
|
||||||
|
- current_period_status calculated when needed
|
||||||
|
- Use Ash calculations for lazy evaluation
|
||||||
|
|
||||||
|
**Pagination:**
|
||||||
|
- Period list paginated if > 50 periods
|
||||||
|
- Member list already paginated
|
||||||
|
|
||||||
|
### Caching Strategy
|
||||||
|
|
||||||
|
**No caching needed in MVP:**
|
||||||
|
- Contribution types rarely change
|
||||||
|
- Period queries are fast
|
||||||
|
- Settings read infrequently
|
||||||
|
|
||||||
|
**Future caching if needed:**
|
||||||
|
- Cache settings in application memory
|
||||||
|
- Cache contribution types list
|
||||||
|
- Invalidate on change
|
||||||
|
|
||||||
|
### Scheduled Job Performance
|
||||||
|
|
||||||
|
**Period Generation Job:**
|
||||||
|
- Run daily or weekly (not hourly)
|
||||||
|
- Batch members (process 100 at a time)
|
||||||
|
- Skip members with no changes
|
||||||
|
- Log failures for retry
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Phase 2: Interval Change Support
|
||||||
|
|
||||||
|
**Architecture Changes:**
|
||||||
|
- Add logic to handle period overlaps
|
||||||
|
- Calculate prorata amounts if needed
|
||||||
|
- More complex validation
|
||||||
|
- Migration path for existing periods
|
||||||
|
|
||||||
|
### Phase 3: Payment Details
|
||||||
|
|
||||||
|
**Architecture Changes:**
|
||||||
|
- Add PaymentTransaction resource
|
||||||
|
- Link transactions to periods
|
||||||
|
- Support multiple payments per period
|
||||||
|
- Reconciliation logic
|
||||||
|
|
||||||
|
### Phase 4: vereinfacht.digital Integration
|
||||||
|
|
||||||
|
**Architecture Changes:**
|
||||||
|
- External API client module
|
||||||
|
- Webhook handling for transactions
|
||||||
|
- Automatic matching logic
|
||||||
|
- Manual review interface
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**End of Architecture Document**
|
||||||
|
|
||||||
527
docs/contributions-overview.md
Normal file
527
docs/contributions-overview.md
Normal file
|
|
@ -0,0 +1,527 @@
|
||||||
|
# Membership Contributions - Overview
|
||||||
|
|
||||||
|
**Project:** Mila - Membership Management System
|
||||||
|
**Feature:** Membership Contribution Management
|
||||||
|
**Version:** 1.0
|
||||||
|
**Last Updated:** 2025-11-27
|
||||||
|
**Status:** Concept - Ready for Review
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document provides a comprehensive overview of the Membership Contributions system. It covers business logic, data model, UI/UX design, and technical architecture in a concise, bullet-point format.
|
||||||
|
|
||||||
|
**For detailed implementation:** See [contributions-implementation-plan.md](./contributions-implementation-plan.md) (created after concept iterations)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Core Principle](#core-principle)
|
||||||
|
2. [Terminology](#terminology)
|
||||||
|
3. [Data Model](#data-model)
|
||||||
|
4. [Business Logic](#business-logic)
|
||||||
|
5. [UI/UX Design](#uiux-design)
|
||||||
|
6. [Edge Cases](#edge-cases)
|
||||||
|
7. [Technical Integration](#technical-integration)
|
||||||
|
8. [Implementation Scope](#implementation-scope)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Principle
|
||||||
|
|
||||||
|
**Maximum Simplicity:**
|
||||||
|
|
||||||
|
- Minimal complexity
|
||||||
|
- Clear data model without redundancies
|
||||||
|
- Intuitive operation
|
||||||
|
- Calendar period-based (Month/Quarter/Half-Year/Year)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Terminology
|
||||||
|
|
||||||
|
### German ↔ English
|
||||||
|
|
||||||
|
**Core Entities:**
|
||||||
|
|
||||||
|
- Beitragsart ↔ Contribution Type / Membership Fee Type
|
||||||
|
- Beitragsintervall ↔ Contribution Period
|
||||||
|
- Mitgliedsbeitrag ↔ Membership Fee / Contribution
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
|
||||||
|
- bezahlt ↔ paid
|
||||||
|
- unbezahlt ↔ unpaid
|
||||||
|
- ausgesetzt ↔ suspended / waived
|
||||||
|
|
||||||
|
**Intervals:**
|
||||||
|
|
||||||
|
- monatlich ↔ monthly
|
||||||
|
- quartalsweise ↔ quarterly
|
||||||
|
- halbjährlich ↔ half-yearly / semi-annually
|
||||||
|
- jährlich ↔ yearly / annually
|
||||||
|
|
||||||
|
**UI Elements:**
|
||||||
|
|
||||||
|
- "Letztes Intervall" ↔ "Last Period" (e.g., 2023 when in 2024)
|
||||||
|
- "Aktuelles Intervall" ↔ "Current Period" (e.g., 2024)
|
||||||
|
- "Als bezahlt markieren" ↔ "Mark as paid"
|
||||||
|
- "Aussetzen" ↔ "Suspend" / "Waive"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
### Contribution Type (ContributionType)
|
||||||
|
|
||||||
|
```
|
||||||
|
- id (UUID)
|
||||||
|
- name (String) - e.g., "Regular", "Reduced", "Student"
|
||||||
|
- amount (Decimal) - Contribution amount in Euro
|
||||||
|
- interval (Enum) - :monthly, :quarterly, :half_yearly, :yearly
|
||||||
|
- description (Text, optional)
|
||||||
|
- timestamps
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:**
|
||||||
|
|
||||||
|
- `interval` is **IMMUTABLE** after creation!
|
||||||
|
- Admin can only change `name`, `amount`, `description`
|
||||||
|
- On change: Future unpaid periods regenerated with new amount
|
||||||
|
|
||||||
|
### Contribution Period (ContributionPeriod)
|
||||||
|
|
||||||
|
```
|
||||||
|
- id (UUID)
|
||||||
|
- member_id (FK → members.id)
|
||||||
|
- contribution_type_id (FK → contribution_types.id)
|
||||||
|
- period_start (Date) - Calendar period start (01.01., 01.04., 01.07., 01.10., etc.)
|
||||||
|
- status (Enum) - :unpaid (default), :paid, :suspended
|
||||||
|
- amount (Decimal) - Amount at generation time (history when type changes)
|
||||||
|
- notes (Text, optional) - Admin notes
|
||||||
|
- timestamps
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:**
|
||||||
|
|
||||||
|
- **NO** `period_end` - calculated from `period_start` + `interval`
|
||||||
|
- **NO** `interval_type` - read from `contribution_type.interval`
|
||||||
|
- Avoids redundancy and inconsistencies!
|
||||||
|
|
||||||
|
**Calendar Period Logic:**
|
||||||
|
|
||||||
|
- Monthly: 01.01. - 31.01., 01.02. - 28./29.02., etc.
|
||||||
|
- Quarterly: 01.01. - 31.03., 01.04. - 30.06., 01.07. - 30.09., 01.10. - 31.12.
|
||||||
|
- Half-yearly: 01.01. - 30.06., 01.07. - 31.12.
|
||||||
|
- Yearly: 01.01. - 31.12.
|
||||||
|
|
||||||
|
### Member (Extensions)
|
||||||
|
|
||||||
|
```
|
||||||
|
- contribution_type_id (FK → contribution_types.id, NOT NULL, default from settings)
|
||||||
|
- contribution_start_date (Date, nullable) - When to start generating contributions
|
||||||
|
- left_at (Date, nullable) - Exit date (existing)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logic for contribution_start_date:**
|
||||||
|
|
||||||
|
- Auto-set based on global setting `include_joining_period`
|
||||||
|
- If `include_joining_period = true`: First day of joining month/quarter/year
|
||||||
|
- If `include_joining_period = false`: First day of NEXT period after joining
|
||||||
|
- Can be manually overridden by admin
|
||||||
|
|
||||||
|
**NO** `include_joining_period` field on Member - unnecessary due to `contribution_start_date`!
|
||||||
|
|
||||||
|
### Global Settings
|
||||||
|
|
||||||
|
```
|
||||||
|
key: "contributions.include_joining_period"
|
||||||
|
value: Boolean (Default: true)
|
||||||
|
|
||||||
|
key: "contributions.default_contribution_type_id"
|
||||||
|
value: UUID (Required) - Default contribution type for new members
|
||||||
|
```
|
||||||
|
|
||||||
|
**Meaning include_joining_period:**
|
||||||
|
|
||||||
|
- `true`: Joining period is included (member pays from joining period)
|
||||||
|
- `false`: Only from next full period after joining
|
||||||
|
|
||||||
|
**Meaning default_contribution_type_id:**
|
||||||
|
|
||||||
|
- Every new member automatically gets this contribution type
|
||||||
|
- Must be configured in admin settings
|
||||||
|
- Prevents: Members without contribution type
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business Logic
|
||||||
|
|
||||||
|
### Period Generation
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
|
||||||
|
- Member gets contribution type assigned (also during member creation)
|
||||||
|
- New period begins (Cron job daily/weekly)
|
||||||
|
- Admin requests manual regeneration
|
||||||
|
|
||||||
|
**Algorithm:**
|
||||||
|
|
||||||
|
1. Get `member.contribution_start_date` and `member.contribution_type`
|
||||||
|
2. Calculate first period based on `contribution_start_date`
|
||||||
|
3. Generate all periods from start to today (or `left_at` if present)
|
||||||
|
4. Skip existing periods
|
||||||
|
5. Set `amount` to current `contribution_type.amount`
|
||||||
|
|
||||||
|
**Example (Yearly):**
|
||||||
|
|
||||||
|
```
|
||||||
|
Joining date: 15.03.2023
|
||||||
|
include_joining_period: true
|
||||||
|
→ contribution_start_date: 01.01.2023
|
||||||
|
|
||||||
|
Generated periods:
|
||||||
|
- 01.01.2023 - 31.12.2023 (joining period)
|
||||||
|
- 01.01.2024 - 31.12.2024
|
||||||
|
- 01.01.2025 - 31.12.2025 (current year)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example (Quarterly):**
|
||||||
|
|
||||||
|
```
|
||||||
|
Joining date: 15.03.2023
|
||||||
|
include_joining_period: false
|
||||||
|
→ contribution_start_date: 01.04.2023
|
||||||
|
|
||||||
|
Generated periods:
|
||||||
|
- 01.04.2023 - 30.06.2023 (first full quarter)
|
||||||
|
- 01.07.2023 - 30.09.2023
|
||||||
|
- 01.10.2023 - 31.12.2023
|
||||||
|
- 01.01.2024 - 31.03.2024
|
||||||
|
- ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status Transitions
|
||||||
|
|
||||||
|
```
|
||||||
|
unpaid → paid
|
||||||
|
unpaid → suspended
|
||||||
|
paid → unpaid
|
||||||
|
suspended → paid
|
||||||
|
suspended → unpaid
|
||||||
|
```
|
||||||
|
|
||||||
|
**Permissions:**
|
||||||
|
|
||||||
|
- Admin + Treasurer (Kassenwart) can change status
|
||||||
|
- Uses existing permission system
|
||||||
|
|
||||||
|
### Contribution Type Change
|
||||||
|
|
||||||
|
**MVP - Same Interval Only:**
|
||||||
|
|
||||||
|
- Member can only choose contribution type with **same interval**
|
||||||
|
- Example: From "Regular (yearly)" to "Reduced (yearly)" ✓
|
||||||
|
- Example: From "Regular (yearly)" to "Reduced (monthly)" ✗
|
||||||
|
|
||||||
|
**Logic on Change:**
|
||||||
|
|
||||||
|
1. Check: New contribution type has same interval
|
||||||
|
2. If yes: Set `member.contribution_type_id`
|
||||||
|
3. Future **unpaid** periods: Delete and regenerate with new amount
|
||||||
|
4. Paid/suspended periods: Remain unchanged (historical amount)
|
||||||
|
|
||||||
|
**Future - Different Intervals:**
|
||||||
|
|
||||||
|
- Enable interval switching (e.g., yearly → monthly)
|
||||||
|
- More complex logic for period overlaps
|
||||||
|
- Needs additional validation
|
||||||
|
|
||||||
|
### Member Exit
|
||||||
|
|
||||||
|
**Logic:**
|
||||||
|
|
||||||
|
- Periods only generated until `member.left_at`
|
||||||
|
- Existing periods remain visible
|
||||||
|
- Unpaid exit period can be marked as "suspended"
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Exit: 15.08.2024
|
||||||
|
Yearly period: 01.01.2024 - 31.12.2024
|
||||||
|
|
||||||
|
→ Period 2024 is shown (Status: unpaid)
|
||||||
|
→ Admin can set to "suspended"
|
||||||
|
→ No periods for 2025+ generated
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UI/UX Design
|
||||||
|
|
||||||
|
### Member List View
|
||||||
|
|
||||||
|
**New Column: "Contribution Status"**
|
||||||
|
|
||||||
|
**Default Display (Last Period):**
|
||||||
|
|
||||||
|
- Shows status of **last completed** period
|
||||||
|
- Example in 2024: Shows contribution for 2023
|
||||||
|
- Color coding:
|
||||||
|
- Green: paid ✓
|
||||||
|
- Red: unpaid ✗
|
||||||
|
- Gray: suspended ⊘
|
||||||
|
|
||||||
|
**Optional: Show Current Period**
|
||||||
|
|
||||||
|
- Toggle: "Show current period" (2024)
|
||||||
|
- Admin decides what to display
|
||||||
|
|
||||||
|
**Filters:**
|
||||||
|
|
||||||
|
- "Unpaid contributions in last period"
|
||||||
|
- "Unpaid contributions in current period"
|
||||||
|
|
||||||
|
### Member Detail View
|
||||||
|
|
||||||
|
**Section: "Contributions"**
|
||||||
|
|
||||||
|
**Contribution Type Assignment:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ Contribution Type: [Dropdown] │
|
||||||
|
│ ⚠ Only types with same interval │
|
||||||
|
│ can be selected │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Period Table:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────┬──────────┬────────┬──────────┬─────────┐
|
||||||
|
│ Period │ Interval │ Amount │ Status │ Action │
|
||||||
|
├───────────────┼──────────┼────────┼──────────┼─────────┤
|
||||||
|
│ 01.01.2023- │ Yearly │ 50 € │ ☑ Paid │ │
|
||||||
|
│ 31.12.2023 │ │ │ │ │
|
||||||
|
├───────────────┼──────────┼────────┼──────────┼─────────┤
|
||||||
|
│ 01.01.2024- │ Yearly │ 60 € │ ☐ Open │ [Mark │
|
||||||
|
│ 31.12.2024 │ │ │ │ as paid]│
|
||||||
|
├───────────────┼──────────┼────────┼──────────┼─────────┤
|
||||||
|
│ 01.01.2025- │ Yearly │ 60 € │ ☐ Open │ [Mark │
|
||||||
|
│ 31.12.2025 │ │ │ │ as paid]│
|
||||||
|
└───────────────┴──────────┴────────┴──────────┴─────────┘
|
||||||
|
|
||||||
|
Legend: ☑ = paid | ☐ = unpaid | ⊘ = suspended
|
||||||
|
```
|
||||||
|
|
||||||
|
**Quick Marking:**
|
||||||
|
|
||||||
|
- Checkbox in each row for fast marking
|
||||||
|
- Button: "Mark selected as paid/unpaid/suspended"
|
||||||
|
- Bulk action for multiple periods
|
||||||
|
|
||||||
|
### Admin: Contribution Types Management
|
||||||
|
|
||||||
|
**List:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────┬──────────┬──────────┬────────────┬─────────┐
|
||||||
|
│ Name │ Amount │ Interval │ Members │ Actions │
|
||||||
|
├────────────┼──────────┼──────────┼────────────┼─────────┤
|
||||||
|
│ Regular │ 60 € │ Yearly │ 45 │ [Edit] │
|
||||||
|
│ Reduced │ 30 € │ Yearly │ 12 │ [Edit] │
|
||||||
|
│ Student │ 20 € │ Monthly │ 8 │ [Edit] │
|
||||||
|
└────────────┴──────────┴──────────┴────────────┴─────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Edit:**
|
||||||
|
|
||||||
|
- Name: ✓ editable
|
||||||
|
- Amount: ✓ editable
|
||||||
|
- Description: ✓ editable
|
||||||
|
- Interval: ✗ **NOT** editable (grayed out)
|
||||||
|
|
||||||
|
**Warning on Amount Change:**
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠ Change amount to 65 €?
|
||||||
|
|
||||||
|
Impact:
|
||||||
|
- 45 members affected
|
||||||
|
- Future unpaid periods will be generated with 65 €
|
||||||
|
- Already paid periods remain with old amount
|
||||||
|
|
||||||
|
[Cancel] [Confirm]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Admin: Settings
|
||||||
|
|
||||||
|
**Contribution Configuration:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Default Contribution Type: [Dropdown: Contribution Types]
|
||||||
|
|
||||||
|
Selected: "Regular (60 €, Yearly)"
|
||||||
|
|
||||||
|
This contribution type is automatically assigned to all new members.
|
||||||
|
Can be changed individually per member.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
☐ Include joining period
|
||||||
|
|
||||||
|
When active:
|
||||||
|
Members pay from the period of their joining.
|
||||||
|
|
||||||
|
Example (Yearly):
|
||||||
|
Joining: 15.03.2023
|
||||||
|
→ Pays from 2023
|
||||||
|
|
||||||
|
When inactive:
|
||||||
|
Members pay from the next full period.
|
||||||
|
|
||||||
|
Example (Yearly):
|
||||||
|
Joining: 15.03.2023
|
||||||
|
→ Pays from 2024
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
|
||||||
|
### 1. Contribution Type Change with Different Interval
|
||||||
|
|
||||||
|
**MVP:** Blocked (only same interval allowed)
|
||||||
|
|
||||||
|
**UI:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Interval change not possible
|
||||||
|
|
||||||
|
Current contribution type: "Regular (Yearly)"
|
||||||
|
Selected contribution type: "Student (Monthly)"
|
||||||
|
|
||||||
|
Changing the interval is currently not possible.
|
||||||
|
Please select a contribution type with interval "Yearly".
|
||||||
|
|
||||||
|
[OK]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Future:**
|
||||||
|
|
||||||
|
- Allow interval switching
|
||||||
|
- Calculate overlaps
|
||||||
|
- Generate new periods without duplicates
|
||||||
|
|
||||||
|
### 2. Exit with Unpaid Contributions
|
||||||
|
|
||||||
|
**Scenario:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Member exits: 15.08.2024
|
||||||
|
Yearly period 2024: unpaid
|
||||||
|
```
|
||||||
|
|
||||||
|
**UI Notice on Exit: (Low Prio)**
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠ Unpaid contributions present
|
||||||
|
|
||||||
|
This member has 1 unpaid period(s):
|
||||||
|
- 2024: 60 € (unpaid)
|
||||||
|
|
||||||
|
Do you want to continue?
|
||||||
|
|
||||||
|
[ ] Mark contribution as "suspended"
|
||||||
|
[Cancel] [Confirm Exit]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Multiple Unpaid Periods
|
||||||
|
|
||||||
|
**Scenario:** Member hasn't paid for 2 years
|
||||||
|
|
||||||
|
**Display:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────┬──────────┬────────┬──────────┬─────────┐
|
||||||
|
│ 2023 │ Yearly │ 50 € │ ☐ Open │ [✓] │
|
||||||
|
│ 2024 │ Yearly │ 60 € │ ☐ Open │ [✓] │
|
||||||
|
│ 2025 │ Yearly │ 60 € │ ☐ Open │ [ ] │
|
||||||
|
└───────────────┴──────────┴────────┴──────────┴─────────┘
|
||||||
|
|
||||||
|
[Mark selected as paid/unpaid/suspended] (2 selected)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Amount Changes
|
||||||
|
|
||||||
|
**Scenario:**
|
||||||
|
|
||||||
|
```
|
||||||
|
2023: Regular = 50 €
|
||||||
|
2024: Regular = 60 € (increase)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result:**
|
||||||
|
|
||||||
|
- Period 2023: Saved with 50 € (history)
|
||||||
|
- Period 2024: Generated with 60 € (current)
|
||||||
|
- Both periods show correct historical amount
|
||||||
|
|
||||||
|
### 5. Date Boundaries
|
||||||
|
|
||||||
|
**Problem:** What if today = 01.01.2025?
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
- Current period (2025) is generated
|
||||||
|
- Status: unpaid (open)
|
||||||
|
- Shown in overview
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Scope
|
||||||
|
|
||||||
|
### MVP (Phase 1)
|
||||||
|
|
||||||
|
**Included:**
|
||||||
|
|
||||||
|
- ✓ Contribution types (CRUD)
|
||||||
|
- ✓ Automatic period generation
|
||||||
|
- ✓ Status management (paid/unpaid/suspended)
|
||||||
|
- ✓ Member overview with contribution status
|
||||||
|
- ✓ Period view per member
|
||||||
|
- ✓ Quick checkbox marking
|
||||||
|
- ✓ Bulk actions
|
||||||
|
- ✓ Amount history
|
||||||
|
- ✓ Same-interval type change
|
||||||
|
- ✓ Default contribution type
|
||||||
|
- ✓ Joining period configuration
|
||||||
|
|
||||||
|
**NOT Included:**
|
||||||
|
|
||||||
|
- ✗ Interval change (only same interval)
|
||||||
|
- ✗ Payment details (date, method)
|
||||||
|
- ✗ Automatic integration (vereinfacht.digital)
|
||||||
|
- ✗ Prorata calculation
|
||||||
|
- ✗ Reports/statistics
|
||||||
|
- ✗ Reminders/dunning (manual via filters)
|
||||||
|
|
||||||
|
### Future Enhancements
|
||||||
|
|
||||||
|
**Phase 2:**
|
||||||
|
|
||||||
|
- Payment details (date, amount, method)
|
||||||
|
- Interval change for future unpaid periods
|
||||||
|
- Manual vereinfacht.digital links per member
|
||||||
|
- Extended filter options
|
||||||
|
|
||||||
|
**Phase 3:**
|
||||||
|
|
||||||
|
- Automated vereinfacht.digital integration
|
||||||
|
- Automatic payment matching
|
||||||
|
- SEPA integration
|
||||||
|
- Advanced reports
|
||||||
|
|
@ -115,7 +115,6 @@ Member (1) → (N) Properties
|
||||||
### Member Constraints
|
### Member Constraints
|
||||||
- First name and last name required (min 1 char)
|
- First name and last name required (min 1 char)
|
||||||
- Email unique, validated format (5-254 chars)
|
- Email unique, validated format (5-254 chars)
|
||||||
- Birth date cannot be in future
|
|
||||||
- Join date cannot be in future
|
- Join date cannot be in future
|
||||||
- Exit date must be after join date
|
- Exit date must be after join date
|
||||||
- Phone: `+?[0-9\- ]{6,20}`
|
- Phone: `+?[0-9\- ]{6,20}`
|
||||||
|
|
@ -169,7 +168,7 @@ 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:** birth_date, phone_number, city, street, house_number, postal_code
|
- **Weight C:** phone_number, city, street, house_number, postal_code
|
||||||
- **Weight D (lowest):** join_date, exit_date
|
- **Weight D (lowest):** join_date, exit_date
|
||||||
|
|
||||||
### Usage Example
|
### Usage Example
|
||||||
|
|
@ -381,7 +380,7 @@ Install "DBML Language" extension to view/edit DBML files with:
|
||||||
- tokens (jti, purpose, extra_data)
|
- tokens (jti, purpose, extra_data)
|
||||||
|
|
||||||
**Personal Data (GDPR):**
|
**Personal Data (GDPR):**
|
||||||
- All member fields (name, email, birth_date, address)
|
- All member fields (name, email, address)
|
||||||
- User email
|
- User email
|
||||||
- Token subject
|
- Token subject
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,6 @@ Table members {
|
||||||
first_name text [not null, note: 'Member first name (min length: 1)']
|
first_name text [not null, note: 'Member first name (min length: 1)']
|
||||||
last_name text [not null, note: 'Member last name (min length: 1)']
|
last_name text [not null, note: 'Member last name (min length: 1)']
|
||||||
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)']
|
||||||
birth_date date [null, note: 'Date of birth (cannot be in future)']
|
|
||||||
paid boolean [null, note: 'Payment status flag']
|
paid boolean [null, note: 'Payment status flag']
|
||||||
phone_number text [null, note: 'Contact phone number (format: +?[0-9\- ]{6,20})']
|
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)']
|
||||||
|
|
@ -153,7 +152,7 @@ Table members {
|
||||||
**Club Member Master Data**
|
**Club Member Master Data**
|
||||||
|
|
||||||
Core entity for membership management containing:
|
Core entity for membership management containing:
|
||||||
- Personal information (name, birth date, email)
|
- Personal information (name, email)
|
||||||
- Contact details (phone, address)
|
- Contact details (phone, address)
|
||||||
- Membership status (join/exit dates, payment status)
|
- Membership status (join/exit dates, payment status)
|
||||||
- Additional notes
|
- Additional notes
|
||||||
|
|
@ -183,7 +182,6 @@ Table members {
|
||||||
**Validation Rules:**
|
**Validation Rules:**
|
||||||
- first_name, last_name: min 1 character
|
- first_name, last_name: min 1 character
|
||||||
- email: 5-254 characters, valid email format
|
- email: 5-254 characters, valid email format
|
||||||
- birth_date: cannot be in future
|
|
||||||
- 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}$
|
- phone_number: matches pattern ^\+?[0-9\- ]{6,20}$
|
||||||
|
|
|
||||||
|
|
@ -100,10 +100,10 @@
|
||||||
**Closed Issues:**
|
**Closed Issues:**
|
||||||
- [#194](https://git.local-it.org/local-it/mitgliederverwaltung/issues/194) - Custom Fields: Harden implementation (S)
|
- [#194](https://git.local-it.org/local-it/mitgliederverwaltung/issues/194) - Custom Fields: Harden implementation (S)
|
||||||
- [#197](https://git.local-it.org/local-it/mitgliederverwaltung/issues/197) - Custom Fields: Add option to show custom fields in member overview (M)
|
- [#197](https://git.local-it.org/local-it/mitgliederverwaltung/issues/197) - Custom Fields: Add option to show custom fields in member overview (M)
|
||||||
|
- [#161](https://git.local-it.org/local-it/mitgliederverwaltung/issues/161) - Remove birthday field from default configuration (S) - Closed 2025-12-02
|
||||||
|
|
||||||
**Open Issues:**
|
**Open Issues:**
|
||||||
- [#157](https://git.local-it.org/local-it/mitgliederverwaltung/issues/157) - Concept how custom fields are handled (M, High priority) [0/4 tasks]
|
- [#157](https://git.local-it.org/local-it/mitgliederverwaltung/issues/157) - Concept how custom fields are handled (M, High priority) [0/4 tasks]
|
||||||
- [#161](https://git.local-it.org/local-it/mitgliederverwaltung/issues/161) - Don't show birthday field for default configurations (S, Low priority)
|
|
||||||
- [#153](https://git.local-it.org/local-it/mitgliederverwaltung/issues/153) - Sorting functionalities for custom fields (M, Low priority)
|
- [#153](https://git.local-it.org/local-it/mitgliederverwaltung/issues/153) - Sorting functionalities for custom fields (M, Low priority)
|
||||||
|
|
||||||
**Missing Features:**
|
**Missing Features:**
|
||||||
|
|
@ -187,10 +187,16 @@
|
||||||
|
|
||||||
**Current State:**
|
**Current State:**
|
||||||
- ✅ Basic "paid" boolean field on members
|
- ✅ Basic "paid" boolean field on members
|
||||||
|
- ✅ **UI Mock-ups for Contribution Types & Settings** (2025-12-02)
|
||||||
- ⚠️ No payment tracking
|
- ⚠️ No payment tracking
|
||||||
|
|
||||||
**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/Contribution Mockup Pages (Preview)
|
||||||
|
|
||||||
|
**Mock-Up Pages (Non-Functional Preview):**
|
||||||
|
- `/contribution_types` - Contribution Types Management
|
||||||
|
- `/contribution_settings` - Global Contribution Settings
|
||||||
|
|
||||||
**Missing Features:**
|
**Missing Features:**
|
||||||
- ❌ Membership fee configuration
|
- ❌ Membership fee configuration
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ defmodule Mv.Membership.Member do
|
||||||
- Email format validation (using EctoCommons.EmailValidator)
|
- Email format validation (using EctoCommons.EmailValidator)
|
||||||
- Phone number format: international format with 6-20 digits
|
- Phone number format: international format with 6-20 digits
|
||||||
- Postal code format: exactly 5 digits (German format)
|
- Postal code format: exactly 5 digits (German format)
|
||||||
- Date validations: birth_date and join_date not in future, exit_date after join_date
|
- Date validations: join_date not in future, exit_date after join_date
|
||||||
- Email uniqueness: prevents conflicts with unlinked users
|
- Email uniqueness: prevents conflicts with unlinked users
|
||||||
|
|
||||||
## Full-Text Search
|
## Full-Text Search
|
||||||
|
|
@ -284,11 +284,6 @@ defmodule Mv.Membership.Member do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Birth date not in the future
|
|
||||||
validate compare(:birth_date, less_than_or_equal_to: &Date.utc_today/0),
|
|
||||||
where: [present(:birth_date)],
|
|
||||||
message: "cannot be in the future"
|
|
||||||
|
|
||||||
# Join date not in the future
|
# Join date not in the future
|
||||||
validate compare(:join_date, less_than_or_equal_to: &Date.utc_today/0),
|
validate compare(:join_date, less_than_or_equal_to: &Date.utc_today/0),
|
||||||
where: [present(:join_date)],
|
where: [present(:join_date)],
|
||||||
|
|
@ -351,10 +346,6 @@ defmodule Mv.Membership.Member do
|
||||||
constraints min_length: 5, max_length: 254
|
constraints min_length: 5, max_length: 254
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :birth_date, :date do
|
|
||||||
allow_nil? true
|
|
||||||
end
|
|
||||||
|
|
||||||
attribute :paid, :boolean do
|
attribute :paid, :boolean do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ defmodule Mv.Constants do
|
||||||
:first_name,
|
:first_name,
|
||||||
:last_name,
|
:last_name,
|
||||||
:email,
|
:email,
|
||||||
:birth_date,
|
|
||||||
:paid,
|
:paid,
|
||||||
:phone_number,
|
:phone_number,
|
||||||
:join_date,
|
:join_date,
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,17 @@ defmodule MvWeb.Layouts.Navbar do
|
||||||
<li><.link navigate="/members">{gettext("Members")}</.link></li>
|
<li><.link navigate="/members">{gettext("Members")}</.link></li>
|
||||||
<li><.link navigate="/custom_fields">{gettext("Custom Fields")}</.link></li>
|
<li><.link navigate="/custom_fields">{gettext("Custom Fields")}</.link></li>
|
||||||
<li><.link navigate="/users">{gettext("Users")}</.link></li>
|
<li><.link navigate="/users">{gettext("Users")}</.link></li>
|
||||||
|
<li>
|
||||||
|
<details>
|
||||||
|
<summary>{gettext("Contributions")}</summary>
|
||||||
|
<ul class="bg-base-200 rounded-t-none p-2 z-10 w-48">
|
||||||
|
<li><.link navigate="/contribution_types">{gettext("Contribution Types")}</.link></li>
|
||||||
|
<li>
|
||||||
|
<.link navigate="/contribution_settings">{gettext("Contribution Settings")}</.link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
|
|
||||||
146
lib/mv_web/live/components/payment_filter_component.ex
Normal file
146
lib/mv_web/live/components/payment_filter_component.ex
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
defmodule MvWeb.Components.PaymentFilterComponent do
|
||||||
|
@moduledoc """
|
||||||
|
Provides the PaymentFilter Live-Component.
|
||||||
|
|
||||||
|
A dropdown filter for filtering members by payment status (paid/not paid/all).
|
||||||
|
Uses DaisyUI dropdown styling and sends filter changes to parent LiveView.
|
||||||
|
|
||||||
|
## Props
|
||||||
|
- `:paid_filter` - Current filter state: `nil` (all), `:paid`, or `:not_paid`
|
||||||
|
- `:id` - Component ID (required)
|
||||||
|
- `:member_count` - Number of filtered members to display in badge (optional, default: 0)
|
||||||
|
|
||||||
|
## Events
|
||||||
|
- Sends `{:payment_filter_changed, filter}` to parent when filter changes
|
||||||
|
"""
|
||||||
|
use MvWeb, :live_component
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(socket) do
|
||||||
|
{:ok, assign(socket, :open, false)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def update(assigns, socket) do
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:id, assigns.id)
|
||||||
|
|> assign(:paid_filter, assigns[:paid_filter])
|
||||||
|
|> assign(:member_count, assigns[:member_count] || 0)
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
|
id={@id}
|
||||||
|
phx-window-keydown={@open && "close_dropdown"}
|
||||||
|
phx-key="Escape"
|
||||||
|
phx-target={@myself}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={[
|
||||||
|
"btn btn-ghost gap-2",
|
||||||
|
@paid_filter && "btn-active"
|
||||||
|
]}
|
||||||
|
phx-click="toggle_dropdown"
|
||||||
|
phx-target={@myself}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={to_string(@open)}
|
||||||
|
aria-label={gettext("Filter by payment status")}
|
||||||
|
>
|
||||||
|
<.icon name="hero-funnel" class="h-5 w-5" />
|
||||||
|
<span class="hidden sm:inline">{filter_label(@paid_filter)}</span>
|
||||||
|
<span :if={@paid_filter} class="badge badge-primary badge-sm">{@member_count}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
:if={@open}
|
||||||
|
class="menu dropdown-content bg-base-100 rounded-box z-10 w-52 p-2 shadow-lg absolute right-0 mt-2"
|
||||||
|
role="menu"
|
||||||
|
aria-label={gettext("Payment filter")}
|
||||||
|
phx-click-away="close_dropdown"
|
||||||
|
phx-target={@myself}
|
||||||
|
>
|
||||||
|
<li role="none">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitemradio"
|
||||||
|
aria-checked={to_string(@paid_filter == nil)}
|
||||||
|
class={@paid_filter == nil && "active"}
|
||||||
|
phx-click="select_filter"
|
||||||
|
phx-value-filter=""
|
||||||
|
phx-target={@myself}
|
||||||
|
>
|
||||||
|
<.icon name="hero-users" class="h-4 w-4" />
|
||||||
|
{gettext("All")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li role="none">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitemradio"
|
||||||
|
aria-checked={to_string(@paid_filter == :paid)}
|
||||||
|
class={@paid_filter == :paid && "active"}
|
||||||
|
phx-click="select_filter"
|
||||||
|
phx-value-filter="paid"
|
||||||
|
phx-target={@myself}
|
||||||
|
>
|
||||||
|
<.icon name="hero-check-circle" class="h-4 w-4 text-success" />
|
||||||
|
{gettext("Paid")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li role="none">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitemradio"
|
||||||
|
aria-checked={to_string(@paid_filter == :not_paid)}
|
||||||
|
class={@paid_filter == :not_paid && "active"}
|
||||||
|
phx-click="select_filter"
|
||||||
|
phx-value-filter="not_paid"
|
||||||
|
phx-target={@myself}
|
||||||
|
>
|
||||||
|
<.icon name="hero-x-circle" class="h-4 w-4 text-error" />
|
||||||
|
{gettext("Not paid")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("toggle_dropdown", _params, socket) do
|
||||||
|
{:noreply, assign(socket, :open, !socket.assigns.open)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("close_dropdown", _params, socket) do
|
||||||
|
{:noreply, assign(socket, :open, false)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("select_filter", %{"filter" => filter_str}, socket) do
|
||||||
|
filter = parse_filter(filter_str)
|
||||||
|
|
||||||
|
# Close dropdown and notify parent
|
||||||
|
socket = assign(socket, :open, false)
|
||||||
|
send(self(), {:payment_filter_changed, filter})
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Parse filter string to atom
|
||||||
|
defp parse_filter("paid"), do: :paid
|
||||||
|
defp parse_filter("not_paid"), do: :not_paid
|
||||||
|
defp parse_filter(_), do: nil
|
||||||
|
|
||||||
|
# Get display label for current filter
|
||||||
|
defp filter_label(nil), do: gettext("All")
|
||||||
|
defp filter_label(:paid), do: gettext("Paid")
|
||||||
|
defp filter_label(:not_paid), do: gettext("Not paid")
|
||||||
|
end
|
||||||
375
lib/mv_web/live/contribution_period_live/show.ex
Normal file
375
lib/mv_web/live/contribution_period_live/show.ex
Normal file
|
|
@ -0,0 +1,375 @@
|
||||||
|
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: "#{@member.first_name} #{@member.last_name}")}
|
||||||
|
<: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"/contribution_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="card bg-base-100 shadow mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-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="card bg-base-200 mb-6">
|
||||||
|
<div class="card-body py-4">
|
||||||
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
|
<span class="font-semibold">{gettext("Change Contribution Type")}:</span>
|
||||||
|
<select class="select select-bordered select-sm w-64" 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="size-4 inline" />
|
||||||
|
{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="badge badge-info badge-sm mt-1">
|
||||||
|
{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 text-base-content/60 italic">
|
||||||
|
{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>
|
||||||
|
|
||||||
|
<.navigation_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
|
||||||
|
|
||||||
|
# Status badge component
|
||||||
|
attr :status, :atom, required: true
|
||||||
|
|
||||||
|
defp status_badge(%{status: :paid} = assigns) do
|
||||||
|
~H"""
|
||||||
|
<span class="badge badge-success gap-1">
|
||||||
|
<.icon name="hero-check-circle-mini" class="size-3" />
|
||||||
|
{gettext("Paid")}
|
||||||
|
</span>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp status_badge(%{status: :unpaid} = assigns) do
|
||||||
|
~H"""
|
||||||
|
<span class="badge badge-error gap-1">
|
||||||
|
<.icon name="hero-x-circle-mini" class="size-3" />
|
||||||
|
{gettext("Unpaid")}
|
||||||
|
</span>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp status_badge(%{status: :suspended} = assigns) do
|
||||||
|
~H"""
|
||||||
|
<span class="badge badge-neutral gap-1">
|
||||||
|
<.icon name="hero-pause-circle-mini" class="size-3" />
|
||||||
|
{gettext("Suspended")}
|
||||||
|
</span>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
# Navigation card
|
||||||
|
defp navigation_card(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="card bg-base-100 shadow mt-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">
|
||||||
|
<.icon name="hero-arrow-right-circle" class="size-5" />
|
||||||
|
{gettext("Related Pages")}
|
||||||
|
</h2>
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<.link navigate={~p"/contribution_types"} class="btn btn-outline btn-sm">
|
||||||
|
<.icon name="hero-tag" class="size-4" />
|
||||||
|
{gettext("Contribution Types")}
|
||||||
|
</.link>
|
||||||
|
<.link navigate={~p"/contribution_settings"} class="btn btn-outline btn-sm">
|
||||||
|
<.icon name="hero-cog-6-tooth" class="size-4" />
|
||||||
|
{gettext("Contribution Settings")}
|
||||||
|
</.link>
|
||||||
|
<.link navigate={~p"/members"} class="btn btn-outline btn-sm">
|
||||||
|
<.icon name="hero-users" class="size-4" />
|
||||||
|
{gettext("Members")}
|
||||||
|
</.link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
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
|
||||||
303
lib/mv_web/live/contribution_settings_live.ex
Normal file
303
lib/mv_web/live/contribution_settings_live.ex
Normal file
|
|
@ -0,0 +1,303 @@
|
||||||
|
defmodule MvWeb.ContributionSettingsLive do
|
||||||
|
@moduledoc """
|
||||||
|
Mock-up LiveView for Contribution Settings (Admin).
|
||||||
|
|
||||||
|
This is a preview-only page that displays the planned UI for managing
|
||||||
|
global contribution settings. It shows static mock data and is not functional.
|
||||||
|
|
||||||
|
## Planned Features (Future Implementation)
|
||||||
|
- Set default contribution type for new members
|
||||||
|
- Configure whether joining period is included in contributions
|
||||||
|
- Explanatory text with examples
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
- `default_contribution_type_id` - UUID of the default contribution type
|
||||||
|
- `include_joining_period` - Boolean whether to include joining 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("Contribution Settings"))
|
||||||
|
|> assign(:contribution_types, mock_contribution_types())
|
||||||
|
|> assign(:selected_type_id, "1")
|
||||||
|
|> assign(:include_joining_period, true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||||
|
<.mockup_warning />
|
||||||
|
|
||||||
|
<.header>
|
||||||
|
{gettext("Contribution Settings")}
|
||||||
|
<:subtitle>
|
||||||
|
{gettext("Configure global settings for membership contributions.")}
|
||||||
|
</:subtitle>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<div class="grid gap-6 lg:grid-cols-2">
|
||||||
|
<%!-- Settings Form --%>
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">
|
||||||
|
<.icon name="hero-cog-6-tooth" class="size-5" />
|
||||||
|
{gettext("Global Settings")}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<form class="space-y-6">
|
||||||
|
<%!-- Default Contribution Type --%>
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-semibold">
|
||||||
|
{gettext("Default Contribution Type")}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<select class="select select-bordered w-full" disabled>
|
||||||
|
<option :for={ct <- @contribution_types} selected={ct.id == @selected_type_id}>
|
||||||
|
{ct.name} ({format_currency(ct.amount)}, {format_interval(ct.interval)})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p class="text-sm text-base-content/60 mt-2">
|
||||||
|
{gettext(
|
||||||
|
"This contribution type is automatically assigned to all new members. Can be changed individually per member."
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<%!-- Include Joining Period --%>
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<label class="label cursor-pointer justify-start gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-primary"
|
||||||
|
checked={@include_joining_period}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<span class="label-text font-semibold">
|
||||||
|
{gettext("Include joining period")}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div class="ml-9 space-y-2">
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
{gettext("When active: Members pay from the period of their joining.")}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
{gettext("When inactive: Members pay from the next full period after joining.")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-primary w-full" disabled>
|
||||||
|
<.icon name="hero-check" class="size-5" />
|
||||||
|
{gettext("Save Settings")}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%!-- Examples Card --%>
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">
|
||||||
|
<.icon name="hero-light-bulb" class="size-5" />
|
||||||
|
{gettext("Examples")}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<.example_section
|
||||||
|
title={gettext("Yearly Interval - Joining Period Included")}
|
||||||
|
joining_date="15.03.2023"
|
||||||
|
include_joining={true}
|
||||||
|
start_date="01.01.2023"
|
||||||
|
periods={["2023", "2024", "2025"]}
|
||||||
|
note={gettext("Member pays for the year they joined")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<.example_section
|
||||||
|
title={gettext("Yearly Interval - Joining Period Excluded")}
|
||||||
|
joining_date="15.03.2023"
|
||||||
|
include_joining={false}
|
||||||
|
start_date="01.01.2024"
|
||||||
|
periods={["2024", "2025"]}
|
||||||
|
note={gettext("Member pays from the next full year")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<.example_section
|
||||||
|
title={gettext("Quarterly Interval - Joining Period Excluded")}
|
||||||
|
joining_date="15.05.2024"
|
||||||
|
include_joining={false}
|
||||||
|
start_date="01.07.2024"
|
||||||
|
periods={["Q3/2024", "Q4/2024", "Q1/2025"]}
|
||||||
|
note={gettext("Member pays from the next full quarter")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<.example_section
|
||||||
|
title={gettext("Monthly Interval - Joining Period Included")}
|
||||||
|
joining_date="15.03.2024"
|
||||||
|
include_joining={true}
|
||||||
|
start_date="01.03.2024"
|
||||||
|
periods={["03/2024", "04/2024", "05/2024", "..."]}
|
||||||
|
note={gettext("Member pays from the joining month")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<.example_member_card />
|
||||||
|
|
||||||
|
<.navigation_card />
|
||||||
|
</Layouts.app>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
# Example member card with link to period view
|
||||||
|
defp example_member_card(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="card bg-base-100 shadow-xl mt-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">
|
||||||
|
<.icon name="hero-user" class="size-5" />
|
||||||
|
{gettext("Example: Member Contribution View")}
|
||||||
|
</h2>
|
||||||
|
<p class="text-base-content/70">
|
||||||
|
{gettext(
|
||||||
|
"See how the contribution periods will be displayed for an individual member. This example shows Maria Weber with multiple contribution periods."
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div class="card-actions justify-end">
|
||||||
|
<.link navigate={~p"/contributions/member/example"} class="btn btn-primary btn-sm">
|
||||||
|
<.icon name="hero-eye" class="size-4" />
|
||||||
|
{gettext("View Example Member")}
|
||||||
|
</.link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
# Example section component
|
||||||
|
attr :title, :string, required: true
|
||||||
|
attr :joining_date, :string, required: true
|
||||||
|
attr :include_joining, :boolean, required: true
|
||||||
|
attr :start_date, :string, required: true
|
||||||
|
attr :periods, :list, required: true
|
||||||
|
attr :note, :string, required: true
|
||||||
|
|
||||||
|
defp example_section(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h3 class="font-semibold text-sm">{@title}</h3>
|
||||||
|
<div class="bg-base-300 rounded-lg p-3 text-sm space-y-1">
|
||||||
|
<p>
|
||||||
|
<span class="text-base-content/60">{gettext("Joining date")}:</span>
|
||||||
|
<span class="font-mono">{@joining_date}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span class="text-base-content/60">{gettext("Contribution start")}:</span>
|
||||||
|
<span class="font-mono font-semibold text-primary">{@start_date}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span class="text-base-content/60">{gettext("Generated periods")}:</span>
|
||||||
|
<span class="font-mono">
|
||||||
|
{Enum.join(@periods, ", ")}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-base-content/60 italic">→ {@note}</p>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
# Navigation card to other contribution pages
|
||||||
|
defp navigation_card(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="card bg-base-100 shadow mt-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">
|
||||||
|
<.icon name="hero-arrow-right-circle" class="size-5" />
|
||||||
|
{gettext("Related Pages")}
|
||||||
|
</h2>
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<.link navigate={~p"/contribution_types"} class="btn btn-outline btn-sm">
|
||||||
|
<.icon name="hero-tag" class="size-4" />
|
||||||
|
{gettext("Contribution Types")}
|
||||||
|
</.link>
|
||||||
|
<.link navigate={~p"/settings"} class="btn btn-outline btn-sm">
|
||||||
|
<.icon name="hero-cog-6-tooth" class="size-4" />
|
||||||
|
{gettext("Club Settings")}
|
||||||
|
</.link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
# Mock data for demonstration
|
||||||
|
defp mock_contribution_types do
|
||||||
|
[
|
||||||
|
%{
|
||||||
|
id: "1",
|
||||||
|
name: gettext("Regular"),
|
||||||
|
amount: Decimal.new("60.00"),
|
||||||
|
interval: :yearly
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
id: "2",
|
||||||
|
name: gettext("Reduced"),
|
||||||
|
amount: Decimal.new("30.00"),
|
||||||
|
interval: :yearly
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
id: "3",
|
||||||
|
name: gettext("Student"),
|
||||||
|
amount: Decimal.new("5.00"),
|
||||||
|
interval: :monthly
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
id: "4",
|
||||||
|
name: gettext("Family"),
|
||||||
|
amount: Decimal.new("25.00"),
|
||||||
|
interval: :quarterly
|
||||||
|
}
|
||||||
|
]
|
||||||
|
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
|
||||||
205
lib/mv_web/live/contribution_type_live/index.ex
Normal file
205
lib/mv_web/live/contribution_type_live/index.ex
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
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 interval (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
|
||||||
|
|
@ -14,7 +14,7 @@ defmodule MvWeb.MemberLive.Form do
|
||||||
- first_name, last_name, email
|
- first_name, last_name, email
|
||||||
|
|
||||||
**Optional:**
|
**Optional:**
|
||||||
- birth_date, phone_number, address fields (city, street, house_number, postal_code)
|
- phone_number, address fields (city, street, house_number, postal_code)
|
||||||
- join_date, exit_date
|
- join_date, exit_date
|
||||||
- paid status
|
- paid status
|
||||||
- notes
|
- notes
|
||||||
|
|
@ -45,7 +45,6 @@ defmodule MvWeb.MemberLive.Form do
|
||||||
<.input field={@form[:first_name]} label={gettext("First Name")} required />
|
<.input field={@form[:first_name]} label={gettext("First Name")} required />
|
||||||
<.input field={@form[:last_name]} label={gettext("Last Name")} required />
|
<.input field={@form[:last_name]} label={gettext("Last Name")} required />
|
||||||
<.input field={@form[:email]} label={gettext("Email")} required type="email" />
|
<.input field={@form[:email]} label={gettext("Email")} required type="email" />
|
||||||
<.input field={@form[:birth_date]} label={gettext("Birth Date")} type="date" />
|
|
||||||
<.input field={@form[:paid]} label={gettext("Paid")} type="checkbox" />
|
<.input field={@form[:paid]} label={gettext("Paid")} type="checkbox" />
|
||||||
<.input field={@form[:phone_number]} label={gettext("Phone Number")} />
|
<.input field={@form[:phone_number]} label={gettext("Phone Number")} />
|
||||||
<.input field={@form[:join_date]} label={gettext("Join Date")} type="date" />
|
<.input field={@form[:join_date]} label={gettext("Join Date")} type="date" />
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
Initializes the LiveView state.
|
Initializes the LiveView state.
|
||||||
|
|
||||||
Sets up initial assigns for page title, search query, sort configuration,
|
Sets up initial assigns for page title, search query, sort configuration,
|
||||||
and member selection. Actual data loading happens in `handle_params/3`.
|
payment filter, and member selection. Actual data loading happens in `handle_params/3`.
|
||||||
"""
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
|
|
@ -74,6 +74,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|> assign(:query, "")
|
|> assign(:query, "")
|
||||||
|> assign_new(:sort_field, fn -> :first_name end)
|
|> assign_new(:sort_field, fn -> :first_name end)
|
||||||
|> assign_new(:sort_order, fn -> :asc end)
|
|> assign_new(:sort_order, fn -> :asc end)
|
||||||
|
|> assign(:paid_filter, nil)
|
||||||
|> assign(:selected_members, MapSet.new())
|
|> assign(:selected_members, MapSet.new())
|
||||||
|> assign(:custom_fields_visible, custom_fields_visible)
|
|> assign(:custom_fields_visible, custom_fields_visible)
|
||||||
|> assign(:member_fields_visible, get_visible_member_fields(settings))
|
|> assign(:member_fields_visible, get_visible_member_fields(settings))
|
||||||
|
|
@ -207,17 +208,17 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:search_changed, q}, socket) do
|
def handle_info({:search_changed, q}, socket) do
|
||||||
socket = load_members(socket, q)
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:query, q)
|
||||||
|
|> load_members()
|
||||||
|
|
||||||
existing_field_query = socket.assigns.sort_field
|
existing_field_query = socket.assigns.sort_field
|
||||||
existing_sort_query = socket.assigns.sort_order
|
existing_sort_query = socket.assigns.sort_order
|
||||||
|
|
||||||
# Build the URL with queries
|
# Build the URL with queries
|
||||||
query_params = %{
|
query_params =
|
||||||
"query" => q,
|
build_query_params(q, existing_field_query, existing_sort_query, socket.assigns.paid_filter)
|
||||||
"sort_field" => existing_field_query,
|
|
||||||
"sort_order" => existing_sort_query
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set the new path with params
|
# Set the new path with params
|
||||||
new_path = ~p"/members?#{query_params}"
|
new_path = ~p"/members?#{query_params}"
|
||||||
|
|
@ -230,13 +231,38 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:payment_filter_changed, filter}, socket) do
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:paid_filter, filter)
|
||||||
|
|> load_members()
|
||||||
|
|
||||||
|
# Build the URL with all params including new filter
|
||||||
|
query_params =
|
||||||
|
build_query_params(
|
||||||
|
socket.assigns.query,
|
||||||
|
socket.assigns.sort_field,
|
||||||
|
socket.assigns.sort_order,
|
||||||
|
filter
|
||||||
|
)
|
||||||
|
|
||||||
|
new_path = ~p"/members?#{query_params}"
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
push_patch(socket,
|
||||||
|
to: new_path,
|
||||||
|
replace: true
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
# Handle Params from the URL
|
# Handle Params from the URL
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
@doc """
|
@doc """
|
||||||
Handles URL parameter changes.
|
Handles URL parameter changes.
|
||||||
|
|
||||||
Parses query parameters for search query, sort field, and sort order,
|
Parses query parameters for search query, sort field, sort order, and payment filter,
|
||||||
then loads members accordingly. This enables bookmarkable URLs and
|
then loads members accordingly. This enables bookmarkable URLs and
|
||||||
browser back/forward navigation.
|
browser back/forward navigation.
|
||||||
"""
|
"""
|
||||||
|
|
@ -246,7 +272,9 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
socket
|
socket
|
||||||
|> maybe_update_search(params)
|
|> maybe_update_search(params)
|
||||||
|> maybe_update_sort(params)
|
|> maybe_update_sort(params)
|
||||||
|> load_members(params["query"])
|
|> maybe_update_paid_filter(params)
|
||||||
|
|> assign(:query, params["query"])
|
||||||
|
|> load_members()
|
||||||
|> prepare_dynamic_cols()
|
|> prepare_dynamic_cols()
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
|
@ -337,11 +365,13 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
field
|
field
|
||||||
end
|
end
|
||||||
|
|
||||||
query_params = %{
|
query_params =
|
||||||
"query" => socket.assigns.query,
|
build_query_params(
|
||||||
"sort_field" => field_str,
|
socket.assigns.query,
|
||||||
"sort_order" => Atom.to_string(order)
|
field_str,
|
||||||
}
|
Atom.to_string(order),
|
||||||
|
socket.assigns.paid_filter
|
||||||
|
)
|
||||||
|
|
||||||
new_path = ~p"/members?#{query_params}"
|
new_path = ~p"/members?#{query_params}"
|
||||||
|
|
||||||
|
|
@ -352,13 +382,45 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Loads members from the database with custom field values and applies search/sort filters.
|
# Builds URL query parameters map including all filter/sort state.
|
||||||
|
# Converts paid_filter atom to string for URL.
|
||||||
|
defp build_query_params(query, sort_field, sort_order, paid_filter) do
|
||||||
|
field_str =
|
||||||
|
if is_atom(sort_field) do
|
||||||
|
Atom.to_string(sort_field)
|
||||||
|
else
|
||||||
|
sort_field
|
||||||
|
end
|
||||||
|
|
||||||
|
order_str =
|
||||||
|
if is_atom(sort_order) do
|
||||||
|
Atom.to_string(sort_order)
|
||||||
|
else
|
||||||
|
sort_order
|
||||||
|
end
|
||||||
|
|
||||||
|
base_params = %{
|
||||||
|
"query" => query,
|
||||||
|
"sort_field" => field_str,
|
||||||
|
"sort_order" => order_str
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only add paid_filter to URL if it's set
|
||||||
|
case paid_filter do
|
||||||
|
nil -> base_params
|
||||||
|
:paid -> Map.put(base_params, "paid_filter", "paid")
|
||||||
|
:not_paid -> Map.put(base_params, "paid_filter", "not_paid")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Loads members from the database with custom field values and applies search/sort/payment filters.
|
||||||
#
|
#
|
||||||
# Process:
|
# Process:
|
||||||
# 1. Builds base query with selected fields
|
# 1. Builds base query with selected fields
|
||||||
# 2. Loads custom field values for visible custom fields (filtered at database level)
|
# 2. Loads custom field values for visible custom fields (filtered at database level)
|
||||||
# 3. Applies search filter if provided
|
# 3. Applies search filter if provided
|
||||||
# 4. Applies sorting (database-level for regular fields, in-memory for custom fields)
|
# 4. Applies payment status filter if set
|
||||||
|
# 5. Applies sorting (database-level for regular fields, in-memory for custom fields)
|
||||||
#
|
#
|
||||||
# Performance Considerations:
|
# Performance Considerations:
|
||||||
# - Database-level filtering: Custom field values are filtered directly in the database
|
# - Database-level filtering: Custom field values are filtered directly in the database
|
||||||
|
|
@ -370,7 +432,9 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
# consider implementing pagination (see Issue #165).
|
# consider implementing pagination (see Issue #165).
|
||||||
#
|
#
|
||||||
# Returns the socket with `:members` assigned.
|
# Returns the socket with `:members` assigned.
|
||||||
defp load_members(socket, search_query) do
|
defp load_members(socket) do
|
||||||
|
search_query = socket.assigns.query
|
||||||
|
|
||||||
query =
|
query =
|
||||||
Mv.Membership.Member
|
Mv.Membership.Member
|
||||||
|> Ash.Query.new()
|
|> Ash.Query.new()
|
||||||
|
|
@ -383,6 +447,9 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
# Apply the search filter first
|
# Apply the search filter first
|
||||||
query = apply_search_filter(query, search_query)
|
query = apply_search_filter(query, search_query)
|
||||||
|
|
||||||
|
# Apply payment status filter
|
||||||
|
query = apply_paid_filter(query, socket.assigns.paid_filter)
|
||||||
|
|
||||||
# Apply sorting based on current socket state
|
# Apply sorting based on current socket state
|
||||||
# For custom fields, we sort after loading
|
# For custom fields, we sort after loading
|
||||||
{query, sort_after_load} =
|
{query, sort_after_load} =
|
||||||
|
|
@ -457,6 +524,24 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Applies payment status filter to the query.
|
||||||
|
#
|
||||||
|
# Filter values:
|
||||||
|
# - nil: No filter, return all members
|
||||||
|
# - :paid: Only members with paid == true
|
||||||
|
# - :not_paid: Members with paid == false or paid == nil (not paid)
|
||||||
|
defp apply_paid_filter(query, nil), do: query
|
||||||
|
|
||||||
|
defp apply_paid_filter(query, :paid) do
|
||||||
|
Ash.Query.filter(query, expr(paid == true))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_paid_filter(query, :not_paid) do
|
||||||
|
# Include both false and nil as "not paid"
|
||||||
|
# Note: paid != true doesn't work correctly with NULL values in SQL
|
||||||
|
Ash.Query.filter(query, expr(paid == false or is_nil(paid)))
|
||||||
|
end
|
||||||
|
|
||||||
# Functions to toggle sorting order
|
# Functions to toggle sorting order
|
||||||
defp toggle_order(:asc), do: :desc
|
defp toggle_order(:asc), do: :desc
|
||||||
defp toggle_order(:desc), do: :asc
|
defp toggle_order(:desc), do: :asc
|
||||||
|
|
@ -747,6 +832,29 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
socket
|
socket
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Updates paid filter from URL parameters if present.
|
||||||
|
#
|
||||||
|
# Validates the filter value, falling back to nil (no filter) if invalid.
|
||||||
|
defp maybe_update_paid_filter(socket, %{"paid_filter" => filter_str}) do
|
||||||
|
filter = determine_paid_filter(filter_str)
|
||||||
|
assign(socket, :paid_filter, filter)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_update_paid_filter(socket, _params) do
|
||||||
|
# Reset filter if not in URL params
|
||||||
|
assign(socket, :paid_filter, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Determines valid paid filter from URL parameter.
|
||||||
|
#
|
||||||
|
# SECURITY: This function whitelists allowed filter values. Only "paid" and "not_paid"
|
||||||
|
# are accepted - all other input (including malicious strings) falls back to nil.
|
||||||
|
# This ensures no raw user input is ever passed to Ash.Query.filter/2, following
|
||||||
|
# Ash's security recommendation to never pass untrusted input directly to filters.
|
||||||
|
defp determine_paid_filter("paid"), do: :paid
|
||||||
|
defp determine_paid_filter("not_paid"), do: :not_paid
|
||||||
|
defp determine_paid_filter(_), do: nil
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
# Helper Functions for Custom Field Values
|
# Helper Functions for Custom Field Values
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,20 @@
|
||||||
</:actions>
|
</:actions>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-4 items-center">
|
||||||
<.live_component
|
<.live_component
|
||||||
module={MvWeb.Components.SearchBarComponent}
|
module={MvWeb.Components.SearchBarComponent}
|
||||||
id="search-bar"
|
id="search-bar"
|
||||||
query={@query}
|
query={@query}
|
||||||
placeholder={gettext("Search...")}
|
placeholder={gettext("Search...")}
|
||||||
/>
|
/>
|
||||||
|
<.live_component
|
||||||
|
module={MvWeb.Components.PaymentFilterComponent}
|
||||||
|
id="payment-filter"
|
||||||
|
paid_filter={@paid_filter}
|
||||||
|
member_count={length(@members)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<.table
|
<.table
|
||||||
id="members"
|
id="members"
|
||||||
|
|
@ -213,6 +221,14 @@
|
||||||
>
|
>
|
||||||
{member.join_date}
|
{member.join_date}
|
||||||
</:col>
|
</:col>
|
||||||
|
<:col :let={member} label={gettext("Paid")}>
|
||||||
|
<span class={[
|
||||||
|
"badge",
|
||||||
|
if(member.paid == true, do: "badge-success", else: "badge-error")
|
||||||
|
]}>
|
||||||
|
{if member.paid == true, do: gettext("Yes"), else: gettext("No")}
|
||||||
|
</span>
|
||||||
|
</:col>
|
||||||
<:action :let={member}>
|
<:action :let={member}>
|
||||||
<div class="sr-only">
|
<div class="sr-only">
|
||||||
<.link navigate={~p"/members/#{member}"}>{gettext("Show")}</.link>
|
<.link navigate={~p"/members/#{member}"}>{gettext("Show")}</.link>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ defmodule MvWeb.MemberLive.Show do
|
||||||
- Return to member list
|
- Return to member list
|
||||||
|
|
||||||
## Displayed Information
|
## Displayed Information
|
||||||
- Basic: name, email, dates (birth, join, exit)
|
- Basic: name, email, dates (join, exit)
|
||||||
- Contact: phone number
|
- Contact: phone number
|
||||||
- Address: street, house number, postal code, city
|
- Address: street, house number, postal code, city
|
||||||
- Status: paid flag
|
- Status: paid flag
|
||||||
|
|
@ -48,7 +48,6 @@ defmodule MvWeb.MemberLive.Show do
|
||||||
<:item title={gettext("First Name")}>{@member.first_name}</:item>
|
<:item title={gettext("First Name")}>{@member.first_name}</:item>
|
||||||
<:item title={gettext("Last Name")}>{@member.last_name}</:item>
|
<:item title={gettext("Last Name")}>{@member.last_name}</:item>
|
||||||
<:item title={gettext("Email")}>{@member.email}</:item>
|
<:item title={gettext("Email")}>{@member.email}</:item>
|
||||||
<:item title={gettext("Birth Date")}>{@member.birth_date}</:item>
|
|
||||||
<:item title={gettext("Paid")}>
|
<:item title={gettext("Paid")}>
|
||||||
{if @member.paid, do: gettext("Yes"), else: gettext("No")}
|
{if @member.paid, do: gettext("Yes"), else: gettext("No")}
|
||||||
</:item>
|
</:item>
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,11 @@ defmodule MvWeb.Router do
|
||||||
|
|
||||||
live "/settings", GlobalSettingsLive
|
live "/settings", GlobalSettingsLive
|
||||||
|
|
||||||
|
# Contribution Management (Mock-ups)
|
||||||
|
live "/contribution_types", ContributionTypeLive.Index, :index
|
||||||
|
live "/contribution_settings", ContributionSettingsLive
|
||||||
|
live "/contributions/member/:id", ContributionPeriodLive.Show, :show
|
||||||
|
|
||||||
post "/set_locale", LocaleController, :set_locale
|
post "/set_locale", LocaleController, :set_locale
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,12 @@ msgstr ""
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
|
|
||||||
#: lib/mv_web/components/core_components.ex:386
|
#: lib/mv_web/components/core_components.ex:386
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:141
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr "Aktionen"
|
msgstr "Aktionen"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:227
|
#: lib/mv_web/live/member_live/index.html.heex:243
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:72
|
#: lib/mv_web/live/user_live/index.html.heex:72
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
|
|
@ -27,20 +28,22 @@ msgstr "Bist du sicher?"
|
||||||
msgid "Attempting to reconnect"
|
msgid "Attempting to reconnect"
|
||||||
msgstr "Verbindung wird wiederhergestellt"
|
msgstr "Verbindung wird wiederhergestellt"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:54
|
#: lib/mv_web/live/member_live/form.ex:53
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:171
|
#: lib/mv_web/live/member_live/index.html.heex:179
|
||||||
#: lib/mv_web/live/member_live/show.ex:59
|
#: lib/mv_web/live/member_live/show.ex:58
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr "Stadt"
|
msgstr "Stadt"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:229
|
#: lib/mv_web/live/contribution_type_live/index.ex:78
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:245
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:74
|
#: lib/mv_web/live/user_live/index.html.heex:74
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr "Löschen"
|
msgstr "Löschen"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:221
|
#: lib/mv_web/live/contribution_type_live/index.ex:66
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:237
|
||||||
#: lib/mv_web/live/user_live/form.ex:265
|
#: lib/mv_web/live/user_live/form.ex:265
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:66
|
#: lib/mv_web/live/user_live/index.html.heex:66
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -48,13 +51,14 @@ msgid "Edit"
|
||||||
msgstr "Bearbeite"
|
msgstr "Bearbeite"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/show.ex:41
|
#: lib/mv_web/live/member_live/show.ex:41
|
||||||
#: lib/mv_web/live/member_live/show.ex:117
|
#: lib/mv_web/live/member_live/show.ex:116
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Edit Member"
|
msgid "Edit Member"
|
||||||
msgstr "Mitglied bearbeiten"
|
msgstr "Mitglied bearbeiten"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:58
|
||||||
#: lib/mv_web/live/member_live/form.ex:47
|
#: lib/mv_web/live/member_live/form.ex:47
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:99
|
#: lib/mv_web/live/member_live/index.html.heex:107
|
||||||
#: lib/mv_web/live/member_live/show.ex:50
|
#: lib/mv_web/live/member_live/show.ex:50
|
||||||
#: lib/mv_web/live/user_live/form.ex:46
|
#: lib/mv_web/live/user_live/form.ex:46
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:44
|
#: lib/mv_web/live/user_live/index.html.heex:44
|
||||||
|
|
@ -69,9 +73,9 @@ msgstr "E-Mail"
|
||||||
msgid "First Name"
|
msgid "First Name"
|
||||||
msgstr "Vorname"
|
msgstr "Vorname"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:51
|
#: lib/mv_web/live/member_live/form.ex:50
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:207
|
#: lib/mv_web/live/member_live/index.html.heex:215
|
||||||
#: lib/mv_web/live/member_live/show.ex:56
|
#: lib/mv_web/live/member_live/show.ex:55
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Join Date"
|
msgid "Join Date"
|
||||||
msgstr "Beitrittsdatum"
|
msgstr "Beitrittsdatum"
|
||||||
|
|
@ -87,7 +91,7 @@ msgstr "Nachname"
|
||||||
msgid "New Member"
|
msgid "New Member"
|
||||||
msgstr "Neues Mitglied"
|
msgstr "Neues Mitglied"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:218
|
#: lib/mv_web/live/member_live/index.html.heex:234
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
#: lib/mv_web/live/user_live/index.html.heex:63
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Show"
|
msgid "Show"
|
||||||
|
|
@ -108,52 +112,52 @@ msgstr "Keine Internetverbindung gefunden"
|
||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr "schließen"
|
msgstr "schließen"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:48
|
#: lib/mv_web/live/member_live/form.ex:51
|
||||||
#: lib/mv_web/live/member_live/show.ex:51
|
#: lib/mv_web/live/member_live/show.ex:56
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Birth Date"
|
|
||||||
msgstr "Geburtsdatum"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:52
|
|
||||||
#: lib/mv_web/live/member_live/show.ex:57
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Exit Date"
|
msgid "Exit Date"
|
||||||
msgstr "Austrittsdatum"
|
msgstr "Austrittsdatum"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:56
|
#: lib/mv_web/live/member_live/form.ex:55
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:135
|
#: lib/mv_web/live/member_live/index.html.heex:143
|
||||||
#: lib/mv_web/live/member_live/show.ex:61
|
#: lib/mv_web/live/member_live/show.ex:60
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "House Number"
|
msgid "House Number"
|
||||||
msgstr "Hausnummer"
|
msgstr "Hausnummer"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:53
|
#: lib/mv_web/live/contribution_period_live/show.ex:140
|
||||||
#: lib/mv_web/live/member_live/show.ex:58
|
#: lib/mv_web/live/member_live/form.ex:52
|
||||||
|
#: lib/mv_web/live/member_live/show.ex:57
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Notes"
|
msgid "Notes"
|
||||||
msgstr "Notizen"
|
msgstr "Notizen"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:49
|
#: lib/mv_web/live/components/payment_filter_component.ex:94
|
||||||
#: lib/mv_web/live/member_live/show.ex:52
|
#: lib/mv_web/live/components/payment_filter_component.ex:144
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:186
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:243
|
||||||
|
#: lib/mv_web/live/member_live/form.ex:48
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:224
|
||||||
|
#: lib/mv_web/live/member_live/show.ex:51
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Paid"
|
msgid "Paid"
|
||||||
msgstr "Bezahlt"
|
msgstr "Bezahlt"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:50
|
#: lib/mv_web/live/member_live/form.ex:49
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:189
|
#: lib/mv_web/live/member_live/index.html.heex:197
|
||||||
#: lib/mv_web/live/member_live/show.ex:55
|
#: lib/mv_web/live/member_live/show.ex:54
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Phone Number"
|
msgid "Phone Number"
|
||||||
msgstr "Telefonnummer"
|
msgstr "Telefonnummer"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:57
|
#: lib/mv_web/live/member_live/form.ex:56
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:153
|
#: lib/mv_web/live/member_live/index.html.heex:161
|
||||||
#: lib/mv_web/live/member_live/show.ex:62
|
#: lib/mv_web/live/member_live/show.ex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Postal Code"
|
msgid "Postal Code"
|
||||||
msgstr "Postleitzahl"
|
msgstr "Postleitzahl"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:80
|
#: lib/mv_web/live/member_live/form.ex:79
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Save Member"
|
msgid "Save Member"
|
||||||
msgstr "Mitglied speichern"
|
msgstr "Mitglied speichern"
|
||||||
|
|
@ -161,15 +165,15 @@ msgstr "Mitglied speichern"
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:66
|
#: lib/mv_web/live/custom_field_live/form.ex:66
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:74
|
#: lib/mv_web/live/custom_field_value_live/form.ex:74
|
||||||
#: lib/mv_web/live/global_settings_live.ex:55
|
#: lib/mv_web/live/global_settings_live.ex:55
|
||||||
#: lib/mv_web/live/member_live/form.ex:79
|
#: lib/mv_web/live/member_live/form.ex:78
|
||||||
#: lib/mv_web/live/user_live/form.ex:248
|
#: lib/mv_web/live/user_live/form.ex:248
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Saving..."
|
msgid "Saving..."
|
||||||
msgstr "Speichern..."
|
msgstr "Speichern..."
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:55
|
#: lib/mv_web/live/member_live/form.ex:54
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:117
|
#: lib/mv_web/live/member_live/index.html.heex:125
|
||||||
#: lib/mv_web/live/member_live/show.ex:60
|
#: lib/mv_web/live/member_live/show.ex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Street"
|
msgid "Street"
|
||||||
msgstr "Straße"
|
msgstr "Straße"
|
||||||
|
|
@ -179,13 +183,14 @@ msgstr "Straße"
|
||||||
msgid "Id"
|
msgid "Id"
|
||||||
msgstr "ID"
|
msgstr "ID"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:229
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex:61
|
#: lib/mv_web/live/member_live/index/formatter.ex:61
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:52
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No"
|
msgid "No"
|
||||||
msgstr "Nein"
|
msgstr "Nein"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/show.ex:116
|
#: lib/mv_web/live/member_live/show.ex:115
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Show Member"
|
msgid "Show Member"
|
||||||
msgstr "Mitglied anzeigen"
|
msgstr "Mitglied anzeigen"
|
||||||
|
|
@ -195,22 +200,23 @@ msgstr "Mitglied anzeigen"
|
||||||
msgid "This is a member record from your database."
|
msgid "This is a member record from your database."
|
||||||
msgstr "Dies ist ein Mitglied aus deiner Datenbank."
|
msgstr "Dies ist ein Mitglied aus deiner Datenbank."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:229
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex:60
|
#: lib/mv_web/live/member_live/index/formatter.ex:60
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:52
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr "Ja"
|
msgstr "Ja"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:110
|
#: lib/mv_web/live/custom_field_live/form.ex:110
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:233
|
#: lib/mv_web/live/custom_field_value_live/form.ex:233
|
||||||
#: lib/mv_web/live/member_live/form.ex:138
|
#: lib/mv_web/live/member_live/form.ex:137
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "create"
|
msgid "create"
|
||||||
msgstr "erstellt"
|
msgstr "erstellt"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:111
|
#: lib/mv_web/live/custom_field_live/form.ex:111
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:234
|
#: lib/mv_web/live/custom_field_value_live/form.ex:234
|
||||||
#: lib/mv_web/live/member_live/form.ex:139
|
#: lib/mv_web/live/member_live/form.ex:138
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "update"
|
msgid "update"
|
||||||
msgstr "aktualisiert"
|
msgstr "aktualisiert"
|
||||||
|
|
@ -220,7 +226,7 @@ msgstr "aktualisiert"
|
||||||
msgid "Incorrect email or password"
|
msgid "Incorrect email or password"
|
||||||
msgstr "Falsche E-Mail oder Passwort"
|
msgstr "Falsche E-Mail oder Passwort"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:145
|
#: lib/mv_web/live/member_live/form.ex:144
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Member %{action} successfully"
|
msgid "Member %{action} successfully"
|
||||||
msgstr "Mitglied %{action} erfolgreich"
|
msgstr "Mitglied %{action} erfolgreich"
|
||||||
|
|
@ -253,7 +259,7 @@ msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt"
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:69
|
#: lib/mv_web/live/custom_field_live/form.ex:69
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:120
|
#: lib/mv_web/live/custom_field_live/index.ex:120
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
||||||
#: lib/mv_web/live/member_live/form.ex:82
|
#: lib/mv_web/live/member_live/form.ex:81
|
||||||
#: lib/mv_web/live/user_live/form.ex:251
|
#: lib/mv_web/live/user_live/form.ex:251
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
|
|
@ -289,7 +295,7 @@ msgstr "ID"
|
||||||
msgid "Immutable"
|
msgid "Immutable"
|
||||||
msgstr "Unveränderlich"
|
msgstr "Unveränderlich"
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:102
|
#: lib/mv_web/components/layouts/navbar.ex:113
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
msgstr "Abmelden"
|
msgstr "Abmelden"
|
||||||
|
|
@ -306,12 +312,15 @@ msgid "Member"
|
||||||
msgstr "Mitglied"
|
msgstr "Mitglied"
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:25
|
#: lib/mv_web/components/layouts/navbar.ex:25
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:286
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:61
|
||||||
#: lib/mv_web/live/member_live/index.ex:73
|
#: lib/mv_web/live/member_live/index.ex:73
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:3
|
#: lib/mv_web/live/member_live/index.html.heex:3
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Members"
|
msgid "Members"
|
||||||
msgstr "Mitglieder"
|
msgstr "Mitglieder"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:48
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:51
|
#: lib/mv_web/live/custom_field_live/form.ex:51
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
|
|
@ -332,6 +341,7 @@ msgstr "Nicht aktiviert"
|
||||||
msgid "Not set"
|
msgid "Not set"
|
||||||
msgstr "Nicht gesetzt"
|
msgstr "Nicht gesetzt"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:207
|
||||||
#: lib/mv_web/live/user_live/form.ex:107
|
#: lib/mv_web/live/user_live/form.ex:107
|
||||||
#: lib/mv_web/live/user_live/form.ex:115
|
#: lib/mv_web/live/user_live/form.ex:115
|
||||||
#: lib/mv_web/live/user_live/form.ex:224
|
#: lib/mv_web/live/user_live/form.ex:224
|
||||||
|
|
@ -350,7 +360,7 @@ msgstr "OIDC ID"
|
||||||
msgid "Password Authentication"
|
msgid "Password Authentication"
|
||||||
msgstr "Passwort-Authentifizierung"
|
msgstr "Passwort-Authentifizierung"
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:95
|
#: lib/mv_web/components/layouts/navbar.ex:106
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Profil"
|
msgid "Profil"
|
||||||
msgstr "Profil"
|
msgstr "Profil"
|
||||||
|
|
@ -360,17 +370,17 @@ msgstr "Profil"
|
||||||
msgid "Required"
|
msgid "Required"
|
||||||
msgstr "Erforderlich"
|
msgstr "Erforderlich"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:55
|
#: lib/mv_web/live/member_live/index.html.heex:63
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select all members"
|
msgid "Select all members"
|
||||||
msgstr "Alle Mitglieder auswählen"
|
msgstr "Alle Mitglieder auswählen"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:69
|
#: lib/mv_web/live/member_live/index.html.heex:77
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select member"
|
msgid "Select member"
|
||||||
msgstr "Mitglied auswählen"
|
msgstr "Mitglied auswählen"
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:99
|
#: lib/mv_web/components/layouts/navbar.ex:110
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr "Einstellungen"
|
msgstr "Einstellungen"
|
||||||
|
|
@ -510,7 +520,7 @@ msgstr "Benutzer*in wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen
|
||||||
msgid "Linked Member"
|
msgid "Linked Member"
|
||||||
msgstr "Verknüpftes Mitglied"
|
msgstr "Verknüpftes Mitglied"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/show.ex:63
|
#: lib/mv_web/live/member_live/show.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Linked User"
|
msgid "Linked User"
|
||||||
msgstr "Verknüpfte*r Benutzer*in"
|
msgstr "Verknüpfte*r Benutzer*in"
|
||||||
|
|
@ -521,7 +531,7 @@ msgstr "Verknüpfte*r Benutzer*in"
|
||||||
msgid "No member linked"
|
msgid "No member linked"
|
||||||
msgstr "Kein Mitglied verknüpft"
|
msgstr "Kein Mitglied verknüpft"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/show.ex:73
|
#: lib/mv_web/live/member_live/show.ex:72
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No user linked"
|
msgid "No user linked"
|
||||||
msgstr "Keine*r Benutzer*in verknüpft"
|
msgstr "Keine*r Benutzer*in verknüpft"
|
||||||
|
|
@ -538,20 +548,20 @@ msgstr "Zurück zur Mitgliederliste"
|
||||||
msgid "Back to users list"
|
msgid "Back to users list"
|
||||||
msgstr "Zurück zur Benutzer*innen-Liste"
|
msgstr "Zurück zur Benutzer*innen-Liste"
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:33
|
#: lib/mv_web/components/layouts/navbar.ex:44
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:39
|
#: lib/mv_web/components/layouts/navbar.ex:50
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select language"
|
msgid "Select language"
|
||||||
msgstr "Sprache auswählen"
|
msgstr "Sprache auswählen"
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:46
|
#: lib/mv_web/components/layouts/navbar.ex:57
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:66
|
#: lib/mv_web/components/layouts/navbar.ex:77
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Toggle dark mode"
|
msgid "Toggle dark mode"
|
||||||
msgstr "Dunklen Modus umschalten"
|
msgstr "Dunklen Modus umschalten"
|
||||||
|
|
||||||
#: lib/mv_web/live/components/search_bar_component.ex:15
|
#: lib/mv_web/live/components/search_bar_component.ex:15
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:33
|
#: lib/mv_web/live/member_live/index.html.heex:34
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr "Suchen..."
|
msgstr "Suchen..."
|
||||||
|
|
@ -567,7 +577,7 @@ msgstr "Benutzer*innen"
|
||||||
msgid "Click to sort"
|
msgid "Click to sort"
|
||||||
msgstr "Klicke um zu sortieren"
|
msgstr "Klicke um zu sortieren"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:81
|
#: lib/mv_web/live/member_live/index.html.heex:89
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
msgstr "Vorname"
|
msgstr "Vorname"
|
||||||
|
|
@ -608,8 +618,8 @@ msgstr "Diese E-Mail-Adresse ist bereits mit einem anderen OIDC-Konto verknüpft
|
||||||
msgid "Choose a custom field"
|
msgid "Choose a custom field"
|
||||||
msgstr "Wähle ein Benutzerdefiniertes Feld"
|
msgstr "Wähle ein Benutzerdefiniertes Feld"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:59
|
#: lib/mv_web/live/member_live/form.ex:58
|
||||||
#: lib/mv_web/live/member_live/show.ex:78
|
#: lib/mv_web/live/member_live/show.ex:77
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Custom Field Values"
|
msgid "Custom Field Values"
|
||||||
msgstr "Benutzerdefinierte Feldwerte"
|
msgstr "Benutzerdefinierte Feldwerte"
|
||||||
|
|
@ -706,6 +716,7 @@ msgstr "In der Mitglieder-Übersicht anzeigen"
|
||||||
msgid "Association Name"
|
msgid "Association Name"
|
||||||
msgstr "Vereinsname"
|
msgstr "Vereinsname"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:257
|
||||||
#: lib/mv_web/live/global_settings_live.ex:31
|
#: lib/mv_web/live/global_settings_live.ex:31
|
||||||
#: lib/mv_web/live/global_settings_live.ex:41
|
#: lib/mv_web/live/global_settings_live.ex:41
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
|
@ -717,6 +728,7 @@ msgstr "Vereinsdaten"
|
||||||
msgid "Manage global settings for the association."
|
msgid "Manage global settings for the association."
|
||||||
msgstr "Passe übergreifende Einstellungen für den Verein an."
|
msgstr "Passe übergreifende Einstellungen für den Verein an."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:102
|
||||||
#: lib/mv_web/live/global_settings_live.ex:56
|
#: lib/mv_web/live/global_settings_live.ex:56
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Save Settings"
|
msgid "Save Settings"
|
||||||
|
|
@ -777,7 +789,7 @@ msgstr "Mitglied entverknüpfen"
|
||||||
msgid "Unlinking scheduled"
|
msgid "Unlinking scheduled"
|
||||||
msgstr "Entverknüpfung geplant"
|
msgstr "Entverknüpfung geplant"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:164
|
#: lib/mv_web/live/member_live/index.ex:165
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Copied %{count} email address to clipboard"
|
msgid "Copied %{count} email address to clipboard"
|
||||||
msgid_plural "Copied %{count} email addresses to clipboard"
|
msgid_plural "Copied %{count} email addresses to clipboard"
|
||||||
|
|
@ -794,12 +806,12 @@ msgstr "E-Mail-Adressen der ausgewählten Mitglieder kopieren"
|
||||||
msgid "Copy emails"
|
msgid "Copy emails"
|
||||||
msgstr "E-Mails kopieren"
|
msgstr "E-Mails kopieren"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:153
|
#: lib/mv_web/live/member_live/index.ex:154
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No email addresses found"
|
msgid "No email addresses found"
|
||||||
msgstr "Keine E-Mail-Adressen gefunden"
|
msgstr "Keine E-Mail-Adressen gefunden"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:150
|
#: lib/mv_web/live/member_live/index.ex:151
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No members selected"
|
msgid "No members selected"
|
||||||
msgstr "Keine Mitglieder ausgewählt"
|
msgstr "Keine Mitglieder ausgewählt"
|
||||||
|
|
@ -814,7 +826,7 @@ msgstr "E-Mail-Programm mit BCC-Empfänger*innen öffnen"
|
||||||
msgid "Open in email program"
|
msgid "Open in email program"
|
||||||
msgstr "Im E-Mail-Programm öffnen"
|
msgstr "Im E-Mail-Programm öffnen"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:173
|
#: lib/mv_web/live/member_live/index.ex:174
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
|
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
|
||||||
msgstr "Tipp: E-Mail-Adressen ins BCC-Feld einfügen für Datenschutzkonformität"
|
msgstr "Tipp: E-Mail-Adressen ins BCC-Feld einfügen für Datenschutzkonformität"
|
||||||
|
|
@ -831,3 +843,460 @@ msgstr "Felder, die mit einem Sternchen (*) markiert sind, dürfen nicht leer bl
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "This field cannot be empty"
|
msgid "This field cannot be empty"
|
||||||
msgstr "Dieses Feld darf nicht leer bleiben"
|
msgstr "Dieses Feld darf nicht leer bleiben"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:80
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:143
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "All"
|
||||||
|
msgstr "Alle"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:54
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Filter by payment status"
|
||||||
|
msgstr "Nach Zahlungsstatus filtern"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:108
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:145
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Not paid"
|
||||||
|
msgstr "Nicht bezahlt"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:65
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Payment filter"
|
||||||
|
msgstr "Zahlungsfilter"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:113
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "About Contribution Types"
|
||||||
|
msgstr "Über Beitragsarten"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:138
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:53
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Amount"
|
||||||
|
msgstr "Betrag"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:124
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Can be changed at any time. Amount changes affect future periods only."
|
||||||
|
msgstr "Kann jederzeit geändert werden. Betragsänderungen wirken sich nur auf zukünftige Perioden aus."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:77
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Cannot delete - members assigned"
|
||||||
|
msgstr "Löschen nicht möglich - Mitglieder zugewiesen"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:42
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Configure global settings for membership contributions."
|
||||||
|
msgstr "Globale Einstellungen für Mitgliedsbeiträge konfigurieren."
|
||||||
|
|
||||||
|
#: lib/mv_web/components/layouts/navbar.ex:34
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:282
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:27
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:40
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution Settings"
|
||||||
|
msgstr "Beitragseinstellungen"
|
||||||
|
|
||||||
|
#: lib/mv_web/components/layouts/navbar.ex:32
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:278
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:253
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:25
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:36
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution Types"
|
||||||
|
msgstr "Beitragsarten"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:226
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution start"
|
||||||
|
msgstr "Beitragsbeginn"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:117
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
|
||||||
|
msgstr "Beitragsarten definieren verschiedene Mitgliedsbeitragsstrukturen. Jede Art hat ein festes Intervall (monatlich, quartalsweise, halbjährlich, jährlich), das nach der Erstellung nicht mehr geändert werden kann."
|
||||||
|
|
||||||
|
#: lib/mv_web/components/layouts/navbar.ex:30
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contributions"
|
||||||
|
msgstr "Beiträge"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:60
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Default Contribution Type"
|
||||||
|
msgstr "Standard-Beitragsart"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:133
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Deletion"
|
||||||
|
msgstr "Löschung"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:113
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Examples"
|
||||||
|
msgstr "Beispiele"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:288
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:172
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Family"
|
||||||
|
msgstr "Familie"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:128
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Fixed after creation. Members can only switch between types with the same interval."
|
||||||
|
msgstr "Nach der Erstellung unveränderlich. Mitglieder können nur zwischen Arten mit demselben Intervall wechseln."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:230
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Generated periods"
|
||||||
|
msgstr "Generierte Perioden"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:52
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Global Settings"
|
||||||
|
msgstr "Globale Einstellungen"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:373
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:301
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:203
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Half-yearly"
|
||||||
|
msgstr "Halbjährlich"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:181
|
||||||
|
#, 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:87
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:188
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Honorary"
|
||||||
|
msgstr "Ehrenmitglied"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:85
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Include joining period"
|
||||||
|
msgstr "Eintrittsperiode einschließen"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:137
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:57
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:127
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Interval"
|
||||||
|
msgstr "Intervall"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:222
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Joining date"
|
||||||
|
msgstr "Eintrittsdatum"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:38
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Manage contribution types for membership fees."
|
||||||
|
msgstr "Beitragsarten für Mitgliedsbeiträge verwalten."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:122
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member pays for the year they joined"
|
||||||
|
msgstr "Mitglied zahlt für das Eintrittsjahr"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:155
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member pays from the joining month"
|
||||||
|
msgstr "Mitglied zahlt ab dem Eintrittsmonat"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:144
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member pays from the next full quarter"
|
||||||
|
msgstr "Mitglied zahlt ab dem nächsten vollen Quartal"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:133
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member pays from the next full year"
|
||||||
|
msgstr "Mitglied zahlt ab dem nächsten vollen Jahr"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:371
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:299
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:201
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Monthly"
|
||||||
|
msgstr "Monatlich"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:150
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Monthly Interval - Joining Period Included"
|
||||||
|
msgstr "Monatliches Intervall - Eintrittsperiode eingeschlossen"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:165
|
||||||
|
#, 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:123
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Name & Amount"
|
||||||
|
msgstr "Name & Betrag"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:42
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "New Contribution Type"
|
||||||
|
msgstr "Neue Beitragsart"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:189
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "No fee for honorary members"
|
||||||
|
msgstr "Kein Beitrag für Ehrenmitglieder"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:134
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Only possible if no members are assigned to this type."
|
||||||
|
msgstr "Nur möglich, wenn keine Mitglieder dieser Art zugewiesen sind."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:227
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:199
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:97
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Preview Mockup"
|
||||||
|
msgstr "Vorschau-Mockup"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:372
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:300
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:202
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Quarterly"
|
||||||
|
msgstr "Quartalsweise"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:139
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Quarterly Interval - Joining Period Excluded"
|
||||||
|
msgstr "Quartalsintervall - Eintrittsperiode ausgeschlossen"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:173
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Quarterly fee for family memberships"
|
||||||
|
msgstr "Quartalsbeitrag für Familienmitgliedschaften"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:86
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:276
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:156
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Reduced"
|
||||||
|
msgstr "Ermäßigt"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:157
|
||||||
|
#, 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:305
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:270
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:148
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Regular"
|
||||||
|
msgstr "Regulär"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:273
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:248
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Related Pages"
|
||||||
|
msgstr "Verwandte Seiten"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:149
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Standard membership fee for regular members"
|
||||||
|
msgstr "Standard-Mitgliedsbeitrag für reguläre Mitglieder"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:282
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:164
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Student"
|
||||||
|
msgstr "Student*in"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:180
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Supporting Member"
|
||||||
|
msgstr "Fördermitglied"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:69
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "This contribution type is automatically assigned to all new members. Can be changed individually per member."
|
||||||
|
msgstr "Diese Beitragsart wird automatisch allen neuen Mitgliedern zugewiesen. Kann pro Mitglied individuell geändert werden."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:90
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "When active: Members pay from the period of their joining."
|
||||||
|
msgstr "Wenn aktiv: Mitglieder zahlen ab der Periode ihres Eintritts."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:93
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "When inactive: Members pay from the next full period after joining."
|
||||||
|
msgstr "Wenn inaktiv: Mitglieder zahlen ab der nächsten vollen Periode nach dem Eintritt."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:85
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:86
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:87
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:374
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:302
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:204
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Yearly"
|
||||||
|
msgstr "Jährlich"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:128
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Yearly Interval - Joining Period Excluded"
|
||||||
|
msgstr "Jährliches Intervall - Eintrittsperiode ausgeschlossen"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:117
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Yearly Interval - Joining Period Included"
|
||||||
|
msgstr "Jährliches Intervall - Eintrittsperiode eingeschlossen"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:107
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "%{count} period selected"
|
||||||
|
msgid_plural "%{count} periods selected"
|
||||||
|
msgstr[0] "%{count} Periode ausgewählt"
|
||||||
|
msgstr[1] "%{count} Perioden ausgewählt"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:48
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Back to Settings"
|
||||||
|
msgstr "Zurück zu Einstellungen"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:83
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Change Contribution Type"
|
||||||
|
msgstr "Beitragsart ändern"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:62
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution Start"
|
||||||
|
msgstr "Beitragsbeginn"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:41
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution type"
|
||||||
|
msgstr "Beitragsart"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:39
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contributions for %{name}"
|
||||||
|
msgstr "Beiträge für %{name}"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:159
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Current"
|
||||||
|
msgstr "Aktuell"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:175
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Example: Member Contribution View"
|
||||||
|
msgstr "Beispiel: Mitglieder-Beitragsansicht"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:361
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Joining year - reduced to 0"
|
||||||
|
msgstr "Eintrittsjahr - auf 0 reduziert"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:116
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Mark as Paid"
|
||||||
|
msgstr "Als bezahlt markieren"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:120
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Mark as Suspended"
|
||||||
|
msgstr "Als ausgesetzt markieren"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:124
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Mark as Unpaid"
|
||||||
|
msgstr "Als unbezahlt markieren"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:26
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member Contributions"
|
||||||
|
msgstr "Mitgliedsbeiträge"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:43
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member since"
|
||||||
|
msgstr "Mitglied seit"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:331
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Paid via bank transfer"
|
||||||
|
msgstr "Per Überweisung bezahlt"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:178
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "See how the contribution periods will be displayed for an individual member. This example shows Maria Weber with multiple contribution periods."
|
||||||
|
msgstr "Sehen Sie, wie die Beitragsperioden für ein einzelnes Mitglied angezeigt werden. Dieses Beispiel zeigt Maria Weber mit mehreren Beitragsperioden."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:139
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Status"
|
||||||
|
msgstr "Status"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:261
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Suspended"
|
||||||
|
msgstr "Ausgesetzt"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:252
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Unpaid"
|
||||||
|
msgstr "Unbezahlt"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:185
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "View Example Member"
|
||||||
|
msgstr "Beispielmitglied ansehen"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:92
|
||||||
|
#, 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 Zahlungsintervall wechseln (z.B. jährlich zu jährlich). Dies verhindert komplexe Periodenüberschneidungen."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:70
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Open Contributions"
|
||||||
|
msgstr "Offene Beiträge"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:204
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Reopen"
|
||||||
|
msgstr "Wieder öffnen"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:195
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Suspend"
|
||||||
|
msgstr "Aussetzen"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:229
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:201
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:99
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "This page is not functional and only displays the planned features."
|
||||||
|
msgstr "Diese Seite ist nicht funktional und zeigt nur die geplanten Funktionen."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:136
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Time Period"
|
||||||
|
msgstr "Zeitraum"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:66
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Total Contributions"
|
||||||
|
msgstr "Beiträge gesamt"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:98
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Why are not all contribution types shown?"
|
||||||
|
msgstr "Warum werden nicht alle Beitragsarten angezeigt?"
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,12 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/core_components.ex:386
|
#: lib/mv_web/components/core_components.ex:386
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:141
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:227
|
#: lib/mv_web/live/member_live/index.html.heex:243
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:72
|
#: lib/mv_web/live/user_live/index.html.heex:72
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
|
|
@ -28,20 +29,22 @@ msgstr ""
|
||||||
msgid "Attempting to reconnect"
|
msgid "Attempting to reconnect"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:54
|
#: lib/mv_web/live/member_live/form.ex:53
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:171
|
#: lib/mv_web/live/member_live/index.html.heex:179
|
||||||
#: lib/mv_web/live/member_live/show.ex:59
|
#: lib/mv_web/live/member_live/show.ex:58
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:229
|
#: lib/mv_web/live/contribution_type_live/index.ex:78
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:245
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:74
|
#: lib/mv_web/live/user_live/index.html.heex:74
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:221
|
#: lib/mv_web/live/contribution_type_live/index.ex:66
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:237
|
||||||
#: lib/mv_web/live/user_live/form.ex:265
|
#: lib/mv_web/live/user_live/form.ex:265
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:66
|
#: lib/mv_web/live/user_live/index.html.heex:66
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -49,13 +52,14 @@ msgid "Edit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/show.ex:41
|
#: lib/mv_web/live/member_live/show.ex:41
|
||||||
#: lib/mv_web/live/member_live/show.ex:117
|
#: lib/mv_web/live/member_live/show.ex:116
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Edit Member"
|
msgid "Edit Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:58
|
||||||
#: lib/mv_web/live/member_live/form.ex:47
|
#: lib/mv_web/live/member_live/form.ex:47
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:99
|
#: lib/mv_web/live/member_live/index.html.heex:107
|
||||||
#: lib/mv_web/live/member_live/show.ex:50
|
#: lib/mv_web/live/member_live/show.ex:50
|
||||||
#: lib/mv_web/live/user_live/form.ex:46
|
#: lib/mv_web/live/user_live/form.ex:46
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:44
|
#: lib/mv_web/live/user_live/index.html.heex:44
|
||||||
|
|
@ -70,9 +74,9 @@ msgstr ""
|
||||||
msgid "First Name"
|
msgid "First Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:51
|
#: lib/mv_web/live/member_live/form.ex:50
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:207
|
#: lib/mv_web/live/member_live/index.html.heex:215
|
||||||
#: lib/mv_web/live/member_live/show.ex:56
|
#: lib/mv_web/live/member_live/show.ex:55
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Join Date"
|
msgid "Join Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -88,7 +92,7 @@ msgstr ""
|
||||||
msgid "New Member"
|
msgid "New Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:218
|
#: lib/mv_web/live/member_live/index.html.heex:234
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
#: lib/mv_web/live/user_live/index.html.heex:63
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Show"
|
msgid "Show"
|
||||||
|
|
@ -109,52 +113,52 @@ msgstr ""
|
||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:48
|
#: lib/mv_web/live/member_live/form.ex:51
|
||||||
#: lib/mv_web/live/member_live/show.ex:51
|
#: lib/mv_web/live/member_live/show.ex:56
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Birth Date"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:52
|
|
||||||
#: lib/mv_web/live/member_live/show.ex:57
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Exit Date"
|
msgid "Exit Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:56
|
#: lib/mv_web/live/member_live/form.ex:55
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:135
|
#: lib/mv_web/live/member_live/index.html.heex:143
|
||||||
#: lib/mv_web/live/member_live/show.ex:61
|
#: lib/mv_web/live/member_live/show.ex:60
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "House Number"
|
msgid "House Number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:53
|
#: lib/mv_web/live/contribution_period_live/show.ex:140
|
||||||
#: lib/mv_web/live/member_live/show.ex:58
|
#: lib/mv_web/live/member_live/form.ex:52
|
||||||
|
#: lib/mv_web/live/member_live/show.ex:57
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Notes"
|
msgid "Notes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:49
|
#: lib/mv_web/live/components/payment_filter_component.ex:94
|
||||||
#: lib/mv_web/live/member_live/show.ex:52
|
#: lib/mv_web/live/components/payment_filter_component.ex:144
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:186
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:243
|
||||||
|
#: lib/mv_web/live/member_live/form.ex:48
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:224
|
||||||
|
#: lib/mv_web/live/member_live/show.ex:51
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Paid"
|
msgid "Paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:50
|
#: lib/mv_web/live/member_live/form.ex:49
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:189
|
#: lib/mv_web/live/member_live/index.html.heex:197
|
||||||
#: lib/mv_web/live/member_live/show.ex:55
|
#: lib/mv_web/live/member_live/show.ex:54
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Phone Number"
|
msgid "Phone Number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:57
|
#: lib/mv_web/live/member_live/form.ex:56
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:153
|
#: lib/mv_web/live/member_live/index.html.heex:161
|
||||||
#: lib/mv_web/live/member_live/show.ex:62
|
#: lib/mv_web/live/member_live/show.ex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Postal Code"
|
msgid "Postal Code"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:80
|
#: lib/mv_web/live/member_live/form.ex:79
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Save Member"
|
msgid "Save Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -162,15 +166,15 @@ msgstr ""
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:66
|
#: lib/mv_web/live/custom_field_live/form.ex:66
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:74
|
#: lib/mv_web/live/custom_field_value_live/form.ex:74
|
||||||
#: lib/mv_web/live/global_settings_live.ex:55
|
#: lib/mv_web/live/global_settings_live.ex:55
|
||||||
#: lib/mv_web/live/member_live/form.ex:79
|
#: lib/mv_web/live/member_live/form.ex:78
|
||||||
#: lib/mv_web/live/user_live/form.ex:248
|
#: lib/mv_web/live/user_live/form.ex:248
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Saving..."
|
msgid "Saving..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:55
|
#: lib/mv_web/live/member_live/form.ex:54
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:117
|
#: lib/mv_web/live/member_live/index.html.heex:125
|
||||||
#: lib/mv_web/live/member_live/show.ex:60
|
#: lib/mv_web/live/member_live/show.ex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Street"
|
msgid "Street"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -180,13 +184,14 @@ msgstr ""
|
||||||
msgid "Id"
|
msgid "Id"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:229
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex:61
|
#: lib/mv_web/live/member_live/index/formatter.ex:61
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:52
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No"
|
msgid "No"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/show.ex:116
|
#: lib/mv_web/live/member_live/show.ex:115
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Show Member"
|
msgid "Show Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -196,22 +201,23 @@ msgstr ""
|
||||||
msgid "This is a member record from your database."
|
msgid "This is a member record from your database."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:229
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex:60
|
#: lib/mv_web/live/member_live/index/formatter.ex:60
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:52
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:110
|
#: lib/mv_web/live/custom_field_live/form.ex:110
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:233
|
#: lib/mv_web/live/custom_field_value_live/form.ex:233
|
||||||
#: lib/mv_web/live/member_live/form.ex:138
|
#: lib/mv_web/live/member_live/form.ex:137
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "create"
|
msgid "create"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:111
|
#: lib/mv_web/live/custom_field_live/form.ex:111
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:234
|
#: lib/mv_web/live/custom_field_value_live/form.ex:234
|
||||||
#: lib/mv_web/live/member_live/form.ex:139
|
#: lib/mv_web/live/member_live/form.ex:138
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "update"
|
msgid "update"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -221,7 +227,7 @@ msgstr ""
|
||||||
msgid "Incorrect email or password"
|
msgid "Incorrect email or password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:145
|
#: lib/mv_web/live/member_live/form.ex:144
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Member %{action} successfully"
|
msgid "Member %{action} successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -254,7 +260,7 @@ msgstr ""
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:69
|
#: lib/mv_web/live/custom_field_live/form.ex:69
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:120
|
#: lib/mv_web/live/custom_field_live/index.ex:120
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
||||||
#: lib/mv_web/live/member_live/form.ex:82
|
#: lib/mv_web/live/member_live/form.ex:81
|
||||||
#: lib/mv_web/live/user_live/form.ex:251
|
#: lib/mv_web/live/user_live/form.ex:251
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
|
|
@ -290,7 +296,7 @@ msgstr ""
|
||||||
msgid "Immutable"
|
msgid "Immutable"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:102
|
#: lib/mv_web/components/layouts/navbar.ex:113
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -307,12 +313,15 @@ msgid "Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:25
|
#: lib/mv_web/components/layouts/navbar.ex:25
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:286
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:61
|
||||||
#: lib/mv_web/live/member_live/index.ex:73
|
#: lib/mv_web/live/member_live/index.ex:73
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:3
|
#: lib/mv_web/live/member_live/index.html.heex:3
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Members"
|
msgid "Members"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:48
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:51
|
#: lib/mv_web/live/custom_field_live/form.ex:51
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
|
|
@ -333,6 +342,7 @@ msgstr ""
|
||||||
msgid "Not set"
|
msgid "Not set"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:207
|
||||||
#: lib/mv_web/live/user_live/form.ex:107
|
#: lib/mv_web/live/user_live/form.ex:107
|
||||||
#: lib/mv_web/live/user_live/form.ex:115
|
#: lib/mv_web/live/user_live/form.ex:115
|
||||||
#: lib/mv_web/live/user_live/form.ex:224
|
#: lib/mv_web/live/user_live/form.ex:224
|
||||||
|
|
@ -351,7 +361,7 @@ msgstr ""
|
||||||
msgid "Password Authentication"
|
msgid "Password Authentication"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:95
|
#: lib/mv_web/components/layouts/navbar.ex:106
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Profil"
|
msgid "Profil"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -361,17 +371,17 @@ msgstr ""
|
||||||
msgid "Required"
|
msgid "Required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:55
|
#: lib/mv_web/live/member_live/index.html.heex:63
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select all members"
|
msgid "Select all members"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:69
|
#: lib/mv_web/live/member_live/index.html.heex:77
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select member"
|
msgid "Select member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:99
|
#: lib/mv_web/components/layouts/navbar.ex:110
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -511,7 +521,7 @@ msgstr ""
|
||||||
msgid "Linked Member"
|
msgid "Linked Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/show.ex:63
|
#: lib/mv_web/live/member_live/show.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Linked User"
|
msgid "Linked User"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -522,7 +532,7 @@ msgstr ""
|
||||||
msgid "No member linked"
|
msgid "No member linked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/show.ex:73
|
#: lib/mv_web/live/member_live/show.ex:72
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No user linked"
|
msgid "No user linked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -539,20 +549,20 @@ msgstr ""
|
||||||
msgid "Back to users list"
|
msgid "Back to users list"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:33
|
#: lib/mv_web/components/layouts/navbar.ex:44
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:39
|
#: lib/mv_web/components/layouts/navbar.ex:50
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select language"
|
msgid "Select language"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:46
|
#: lib/mv_web/components/layouts/navbar.ex:57
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:66
|
#: lib/mv_web/components/layouts/navbar.ex:77
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Toggle dark mode"
|
msgid "Toggle dark mode"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/components/search_bar_component.ex:15
|
#: lib/mv_web/live/components/search_bar_component.ex:15
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:33
|
#: lib/mv_web/live/member_live/index.html.heex:34
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -568,7 +578,7 @@ msgstr ""
|
||||||
msgid "Click to sort"
|
msgid "Click to sort"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:81
|
#: lib/mv_web/live/member_live/index.html.heex:89
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -609,8 +619,8 @@ msgstr ""
|
||||||
msgid "Choose a custom field"
|
msgid "Choose a custom field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:59
|
#: lib/mv_web/live/member_live/form.ex:58
|
||||||
#: lib/mv_web/live/member_live/show.ex:78
|
#: lib/mv_web/live/member_live/show.ex:77
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Custom Field Values"
|
msgid "Custom Field Values"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -707,6 +717,7 @@ msgstr ""
|
||||||
msgid "Association Name"
|
msgid "Association Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:257
|
||||||
#: lib/mv_web/live/global_settings_live.ex:31
|
#: lib/mv_web/live/global_settings_live.ex:31
|
||||||
#: lib/mv_web/live/global_settings_live.ex:41
|
#: lib/mv_web/live/global_settings_live.ex:41
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -718,6 +729,7 @@ msgstr ""
|
||||||
msgid "Manage global settings for the association."
|
msgid "Manage global settings for the association."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:102
|
||||||
#: lib/mv_web/live/global_settings_live.ex:56
|
#: lib/mv_web/live/global_settings_live.ex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Save Settings"
|
msgid "Save Settings"
|
||||||
|
|
@ -778,7 +790,7 @@ msgstr ""
|
||||||
msgid "Unlinking scheduled"
|
msgid "Unlinking scheduled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:164
|
#: lib/mv_web/live/member_live/index.ex:165
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Copied %{count} email address to clipboard"
|
msgid "Copied %{count} email address to clipboard"
|
||||||
msgid_plural "Copied %{count} email addresses to clipboard"
|
msgid_plural "Copied %{count} email addresses to clipboard"
|
||||||
|
|
@ -795,12 +807,12 @@ msgstr ""
|
||||||
msgid "Copy emails"
|
msgid "Copy emails"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:153
|
#: lib/mv_web/live/member_live/index.ex:154
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No email addresses found"
|
msgid "No email addresses found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:150
|
#: lib/mv_web/live/member_live/index.ex:151
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No members selected"
|
msgid "No members selected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -815,7 +827,7 @@ msgstr ""
|
||||||
msgid "Open in email program"
|
msgid "Open in email program"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:173
|
#: lib/mv_web/live/member_live/index.ex:174
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
|
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -832,3 +844,460 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "This field cannot be empty"
|
msgid "This field cannot be empty"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:80
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:143
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "All"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:54
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Filter by payment status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:108
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:145
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Not paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:65
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Payment filter"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:113
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "About Contribution Types"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:138
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:53
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Amount"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:124
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Can be changed at any time. Amount changes affect future periods only."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:77
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Cannot delete - members assigned"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:42
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Configure global settings for membership contributions."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/components/layouts/navbar.ex:34
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:282
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:27
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:40
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/components/layouts/navbar.ex:32
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:278
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:253
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:25
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:36
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution Types"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:226
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution start"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:117
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/components/layouts/navbar.ex:30
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contributions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:60
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Default Contribution Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:133
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Deletion"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:113
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Examples"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:288
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:172
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Family"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:128
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Fixed after creation. Members can only switch between types with the same interval."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:230
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Generated periods"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:52
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Global Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:373
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:301
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:203
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Half-yearly"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:181
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Half-yearly contribution for supporting members"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:87
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:188
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Honorary"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:85
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Include joining period"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:137
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:57
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:127
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Interval"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:222
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Joining date"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:38
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Manage contribution types for membership fees."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:122
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member pays for the year they joined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:155
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member pays from the joining month"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:144
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member pays from the next full quarter"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:133
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member pays from the next full year"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:371
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:299
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:201
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Monthly"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:150
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Monthly Interval - Joining Period Included"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:165
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Monthly fee for students and trainees"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:123
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Name & Amount"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:42
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "New Contribution Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:189
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "No fee for honorary members"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:134
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Only possible if no members are assigned to this type."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:227
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:199
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:97
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Preview Mockup"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:372
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:300
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:202
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Quarterly"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:139
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Quarterly Interval - Joining Period Excluded"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:173
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Quarterly fee for family memberships"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:86
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:276
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:156
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Reduced"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:157
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Reduced fee for unemployed, pensioners, or low income"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:305
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:270
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:148
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Regular"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:273
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:248
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Related Pages"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:149
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Standard membership fee for regular members"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:282
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:164
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Student"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:180
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Supporting Member"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:69
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "This contribution type is automatically assigned to all new members. Can be changed individually per member."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:90
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "When active: Members pay from the period of their joining."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:93
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "When inactive: Members pay from the next full period after joining."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:85
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:86
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:87
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:374
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:302
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:204
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Yearly"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:128
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Yearly Interval - Joining Period Excluded"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:117
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Yearly Interval - Joining Period Included"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:107
|
||||||
|
#, 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:48
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Back to Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:83
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Change Contribution Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:62
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution Start"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:41
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:39
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contributions for %{name}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:159
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Current"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:175
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Example: Member Contribution View"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:361
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Joining year - reduced to 0"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:116
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Mark as Paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:120
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Mark as Suspended"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:124
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Mark as Unpaid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:26
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member Contributions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:43
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member since"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:331
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Paid via bank transfer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:178
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "See how the contribution periods will be displayed for an individual member. This example shows Maria Weber with multiple contribution periods."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:139
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:261
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Suspended"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:252
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Unpaid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:185
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "View Example Member"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:92
|
||||||
|
#, 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/contribution_period_live/show.ex:70
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Open Contributions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:204
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Reopen"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:195
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Suspend"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:229
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:201
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:99
|
||||||
|
#, 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:136
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Time Period"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:66
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Total Contributions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:98
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Why are not all contribution types shown?"
|
||||||
|
msgstr ""
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,12 @@ msgstr ""
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: lib/mv_web/components/core_components.ex:386
|
#: lib/mv_web/components/core_components.ex:386
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:141
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:227
|
#: lib/mv_web/live/member_live/index.html.heex:243
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:72
|
#: lib/mv_web/live/user_live/index.html.heex:72
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
|
|
@ -28,20 +29,22 @@ msgstr ""
|
||||||
msgid "Attempting to reconnect"
|
msgid "Attempting to reconnect"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:54
|
#: lib/mv_web/live/member_live/form.ex:53
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:171
|
#: lib/mv_web/live/member_live/index.html.heex:179
|
||||||
#: lib/mv_web/live/member_live/show.ex:59
|
#: lib/mv_web/live/member_live/show.ex:58
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:229
|
#: lib/mv_web/live/contribution_type_live/index.ex:78
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:245
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:74
|
#: lib/mv_web/live/user_live/index.html.heex:74
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:221
|
#: lib/mv_web/live/contribution_type_live/index.ex:66
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:237
|
||||||
#: lib/mv_web/live/user_live/form.ex:265
|
#: lib/mv_web/live/user_live/form.ex:265
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:66
|
#: lib/mv_web/live/user_live/index.html.heex:66
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -49,13 +52,14 @@ msgid "Edit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/show.ex:41
|
#: lib/mv_web/live/member_live/show.ex:41
|
||||||
#: lib/mv_web/live/member_live/show.ex:117
|
#: lib/mv_web/live/member_live/show.ex:116
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Edit Member"
|
msgid "Edit Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:58
|
||||||
#: lib/mv_web/live/member_live/form.ex:47
|
#: lib/mv_web/live/member_live/form.ex:47
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:99
|
#: lib/mv_web/live/member_live/index.html.heex:107
|
||||||
#: lib/mv_web/live/member_live/show.ex:50
|
#: lib/mv_web/live/member_live/show.ex:50
|
||||||
#: lib/mv_web/live/user_live/form.ex:46
|
#: lib/mv_web/live/user_live/form.ex:46
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:44
|
#: lib/mv_web/live/user_live/index.html.heex:44
|
||||||
|
|
@ -70,9 +74,9 @@ msgstr ""
|
||||||
msgid "First Name"
|
msgid "First Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:51
|
#: lib/mv_web/live/member_live/form.ex:50
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:207
|
#: lib/mv_web/live/member_live/index.html.heex:215
|
||||||
#: lib/mv_web/live/member_live/show.ex:56
|
#: lib/mv_web/live/member_live/show.ex:55
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Join Date"
|
msgid "Join Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -88,7 +92,7 @@ msgstr ""
|
||||||
msgid "New Member"
|
msgid "New Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:218
|
#: lib/mv_web/live/member_live/index.html.heex:234
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
#: lib/mv_web/live/user_live/index.html.heex:63
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Show"
|
msgid "Show"
|
||||||
|
|
@ -109,52 +113,52 @@ msgstr ""
|
||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:48
|
#: lib/mv_web/live/member_live/form.ex:51
|
||||||
#: lib/mv_web/live/member_live/show.ex:51
|
#: lib/mv_web/live/member_live/show.ex:56
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Birth Date"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:52
|
|
||||||
#: lib/mv_web/live/member_live/show.ex:57
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Exit Date"
|
msgid "Exit Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:56
|
#: lib/mv_web/live/member_live/form.ex:55
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:135
|
#: lib/mv_web/live/member_live/index.html.heex:143
|
||||||
#: lib/mv_web/live/member_live/show.ex:61
|
#: lib/mv_web/live/member_live/show.ex:60
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "House Number"
|
msgid "House Number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:53
|
#: lib/mv_web/live/contribution_period_live/show.ex:140
|
||||||
#: lib/mv_web/live/member_live/show.ex:58
|
#: lib/mv_web/live/member_live/form.ex:52
|
||||||
|
#: lib/mv_web/live/member_live/show.ex:57
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Notes"
|
msgid "Notes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:49
|
#: lib/mv_web/live/components/payment_filter_component.ex:94
|
||||||
#: lib/mv_web/live/member_live/show.ex:52
|
#: lib/mv_web/live/components/payment_filter_component.ex:144
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:186
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:243
|
||||||
|
#: lib/mv_web/live/member_live/form.ex:48
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:224
|
||||||
|
#: lib/mv_web/live/member_live/show.ex:51
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Paid"
|
msgid "Paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:50
|
#: lib/mv_web/live/member_live/form.ex:49
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:189
|
#: lib/mv_web/live/member_live/index.html.heex:197
|
||||||
#: lib/mv_web/live/member_live/show.ex:55
|
#: lib/mv_web/live/member_live/show.ex:54
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Phone Number"
|
msgid "Phone Number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:57
|
#: lib/mv_web/live/member_live/form.ex:56
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:153
|
#: lib/mv_web/live/member_live/index.html.heex:161
|
||||||
#: lib/mv_web/live/member_live/show.ex:62
|
#: lib/mv_web/live/member_live/show.ex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Postal Code"
|
msgid "Postal Code"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:80
|
#: lib/mv_web/live/member_live/form.ex:79
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Save Member"
|
msgid "Save Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -162,15 +166,15 @@ msgstr ""
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:66
|
#: lib/mv_web/live/custom_field_live/form.ex:66
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:74
|
#: lib/mv_web/live/custom_field_value_live/form.ex:74
|
||||||
#: lib/mv_web/live/global_settings_live.ex:55
|
#: lib/mv_web/live/global_settings_live.ex:55
|
||||||
#: lib/mv_web/live/member_live/form.ex:79
|
#: lib/mv_web/live/member_live/form.ex:78
|
||||||
#: lib/mv_web/live/user_live/form.ex:248
|
#: lib/mv_web/live/user_live/form.ex:248
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Saving..."
|
msgid "Saving..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:55
|
#: lib/mv_web/live/member_live/form.ex:54
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:117
|
#: lib/mv_web/live/member_live/index.html.heex:125
|
||||||
#: lib/mv_web/live/member_live/show.ex:60
|
#: lib/mv_web/live/member_live/show.ex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Street"
|
msgid "Street"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -180,13 +184,14 @@ msgstr ""
|
||||||
msgid "Id"
|
msgid "Id"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:229
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex:61
|
#: lib/mv_web/live/member_live/index/formatter.ex:61
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:52
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No"
|
msgid "No"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/show.ex:116
|
#: lib/mv_web/live/member_live/show.ex:115
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Show Member"
|
msgid "Show Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -196,22 +201,23 @@ msgstr ""
|
||||||
msgid "This is a member record from your database."
|
msgid "This is a member record from your database."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex:229
|
||||||
#: lib/mv_web/live/member_live/index/formatter.ex:60
|
#: lib/mv_web/live/member_live/index/formatter.ex:60
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:52
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:110
|
#: lib/mv_web/live/custom_field_live/form.ex:110
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:233
|
#: lib/mv_web/live/custom_field_value_live/form.ex:233
|
||||||
#: lib/mv_web/live/member_live/form.ex:138
|
#: lib/mv_web/live/member_live/form.ex:137
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "create"
|
msgid "create"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:111
|
#: lib/mv_web/live/custom_field_live/form.ex:111
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:234
|
#: lib/mv_web/live/custom_field_value_live/form.ex:234
|
||||||
#: lib/mv_web/live/member_live/form.ex:139
|
#: lib/mv_web/live/member_live/form.ex:138
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "update"
|
msgid "update"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -221,7 +227,7 @@ msgstr ""
|
||||||
msgid "Incorrect email or password"
|
msgid "Incorrect email or password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:145
|
#: lib/mv_web/live/member_live/form.ex:144
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Member %{action} successfully"
|
msgid "Member %{action} successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -254,7 +260,7 @@ msgstr ""
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:69
|
#: lib/mv_web/live/custom_field_live/form.ex:69
|
||||||
#: lib/mv_web/live/custom_field_live/index.ex:120
|
#: lib/mv_web/live/custom_field_live/index.ex:120
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
||||||
#: lib/mv_web/live/member_live/form.ex:82
|
#: lib/mv_web/live/member_live/form.ex:81
|
||||||
#: lib/mv_web/live/user_live/form.ex:251
|
#: lib/mv_web/live/user_live/form.ex:251
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
|
|
@ -290,7 +296,7 @@ msgstr ""
|
||||||
msgid "Immutable"
|
msgid "Immutable"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:102
|
#: lib/mv_web/components/layouts/navbar.ex:113
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -307,12 +313,15 @@ msgid "Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:25
|
#: lib/mv_web/components/layouts/navbar.ex:25
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:286
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:61
|
||||||
#: lib/mv_web/live/member_live/index.ex:73
|
#: lib/mv_web/live/member_live/index.ex:73
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:3
|
#: lib/mv_web/live/member_live/index.html.heex:3
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Members"
|
msgid "Members"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:48
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:51
|
#: lib/mv_web/live/custom_field_live/form.ex:51
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
|
|
@ -333,6 +342,7 @@ msgstr ""
|
||||||
msgid "Not set"
|
msgid "Not set"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:207
|
||||||
#: lib/mv_web/live/user_live/form.ex:107
|
#: lib/mv_web/live/user_live/form.ex:107
|
||||||
#: lib/mv_web/live/user_live/form.ex:115
|
#: lib/mv_web/live/user_live/form.ex:115
|
||||||
#: lib/mv_web/live/user_live/form.ex:224
|
#: lib/mv_web/live/user_live/form.ex:224
|
||||||
|
|
@ -351,7 +361,7 @@ msgstr ""
|
||||||
msgid "Password Authentication"
|
msgid "Password Authentication"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:95
|
#: lib/mv_web/components/layouts/navbar.ex:106
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Profil"
|
msgid "Profil"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -361,17 +371,17 @@ msgstr ""
|
||||||
msgid "Required"
|
msgid "Required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:55
|
#: lib/mv_web/live/member_live/index.html.heex:63
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select all members"
|
msgid "Select all members"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:69
|
#: lib/mv_web/live/member_live/index.html.heex:77
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select member"
|
msgid "Select member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:99
|
#: lib/mv_web/components/layouts/navbar.ex:110
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -511,7 +521,7 @@ msgstr "User will be created without a password. Check 'Set Password' to add one
|
||||||
msgid "Linked Member"
|
msgid "Linked Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/show.ex:63
|
#: lib/mv_web/live/member_live/show.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Linked User"
|
msgid "Linked User"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -522,7 +532,7 @@ msgstr ""
|
||||||
msgid "No member linked"
|
msgid "No member linked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/show.ex:73
|
#: lib/mv_web/live/member_live/show.ex:72
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No user linked"
|
msgid "No user linked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -539,20 +549,20 @@ msgstr ""
|
||||||
msgid "Back to users list"
|
msgid "Back to users list"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:33
|
#: lib/mv_web/components/layouts/navbar.ex:44
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:39
|
#: lib/mv_web/components/layouts/navbar.ex:50
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Select language"
|
msgid "Select language"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:46
|
#: lib/mv_web/components/layouts/navbar.ex:57
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:66
|
#: lib/mv_web/components/layouts/navbar.ex:77
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Toggle dark mode"
|
msgid "Toggle dark mode"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/components/search_bar_component.ex:15
|
#: lib/mv_web/live/components/search_bar_component.ex:15
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:33
|
#: lib/mv_web/live/member_live/index.html.heex:34
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -568,7 +578,7 @@ msgstr ""
|
||||||
msgid "Click to sort"
|
msgid "Click to sort"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:81
|
#: lib/mv_web/live/member_live/index.html.heex:89
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -609,8 +619,8 @@ msgstr ""
|
||||||
msgid "Choose a custom field"
|
msgid "Choose a custom field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:59
|
#: lib/mv_web/live/member_live/form.ex:58
|
||||||
#: lib/mv_web/live/member_live/show.ex:78
|
#: lib/mv_web/live/member_live/show.ex:77
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Custom Field Values"
|
msgid "Custom Field Values"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -707,6 +717,7 @@ msgstr ""
|
||||||
msgid "Association Name"
|
msgid "Association Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:257
|
||||||
#: lib/mv_web/live/global_settings_live.ex:31
|
#: lib/mv_web/live/global_settings_live.ex:31
|
||||||
#: lib/mv_web/live/global_settings_live.ex:41
|
#: lib/mv_web/live/global_settings_live.ex:41
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
|
@ -718,6 +729,7 @@ msgstr ""
|
||||||
msgid "Manage global settings for the association."
|
msgid "Manage global settings for the association."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:102
|
||||||
#: lib/mv_web/live/global_settings_live.ex:56
|
#: lib/mv_web/live/global_settings_live.ex:56
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Save Settings"
|
msgid "Save Settings"
|
||||||
|
|
@ -778,7 +790,7 @@ msgstr ""
|
||||||
msgid "Unlinking scheduled"
|
msgid "Unlinking scheduled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:164
|
#: lib/mv_web/live/member_live/index.ex:165
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Copied %{count} email address to clipboard"
|
msgid "Copied %{count} email address to clipboard"
|
||||||
msgid_plural "Copied %{count} email addresses to clipboard"
|
msgid_plural "Copied %{count} email addresses to clipboard"
|
||||||
|
|
@ -795,12 +807,12 @@ msgstr ""
|
||||||
msgid "Copy emails"
|
msgid "Copy emails"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:153
|
#: lib/mv_web/live/member_live/index.ex:154
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No email addresses found"
|
msgid "No email addresses found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:150
|
#: lib/mv_web/live/member_live/index.ex:151
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "No members selected"
|
msgid "No members selected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -815,7 +827,7 @@ msgstr ""
|
||||||
msgid "Open in email program"
|
msgid "Open in email program"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.ex:173
|
#: lib/mv_web/live/member_live/index.ex:174
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
|
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -832,3 +844,460 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "This field cannot be empty"
|
msgid "This field cannot be empty"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:80
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:143
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "All"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:54
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Filter by payment status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:108
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:145
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Not paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/components/payment_filter_component.ex:65
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Payment filter"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:113
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "About Contribution Types"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:138
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:53
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Amount"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:124
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Can be changed at any time. Amount changes affect future periods only."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:77
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Cannot delete - members assigned"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:42
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Configure global settings for membership contributions."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/components/layouts/navbar.ex:34
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:282
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:27
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:40
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/components/layouts/navbar.ex:32
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:278
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:253
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:25
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:36
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution Types"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:226
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution start"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:117
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/components/layouts/navbar.ex:30
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Contributions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:60
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Default Contribution Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:133
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Deletion"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:113
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Examples"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:288
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:172
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Family"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:128
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Fixed after creation. Members can only switch between types with the same interval."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:230
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Generated periods"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:52
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Global Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:373
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:301
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:203
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Half-yearly"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:181
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Half-yearly contribution for supporting members"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:87
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:188
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Honorary"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:85
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Include joining period"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:137
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:57
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:127
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Interval"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:222
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Joining date"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:38
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Manage contribution types for membership fees."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:122
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member pays for the year they joined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:155
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member pays from the joining month"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:144
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member pays from the next full quarter"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:133
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member pays from the next full year"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:371
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:299
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:201
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Monthly"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:150
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Monthly Interval - Joining Period Included"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:165
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Monthly fee for students and trainees"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:123
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Name & Amount"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:42
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "New Contribution Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:189
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "No fee for honorary members"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:134
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Only possible if no members are assigned to this type."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:227
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:199
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:97
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Preview Mockup"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:372
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:300
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:202
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Quarterly"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:139
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Quarterly Interval - Joining Period Excluded"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:173
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Quarterly fee for family memberships"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:86
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:276
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:156
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Reduced"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:157
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Reduced fee for unemployed, pensioners, or low income"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:305
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:270
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:148
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Regular"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:273
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:248
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Related Pages"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:149
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Standard membership fee for regular members"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:282
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:164
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Student"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:180
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Supporting Member"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:69
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "This contribution type is automatically assigned to all new members. Can be changed individually per member."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:90
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "When active: Members pay from the period of their joining."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:93
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "When inactive: Members pay from the next full period after joining."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:85
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:86
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:87
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:374
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:302
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:204
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Yearly"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:128
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Yearly Interval - Joining Period Excluded"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:117
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Yearly Interval - Joining Period Included"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:107
|
||||||
|
#, 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:48
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Back to Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:83
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Change Contribution Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:62
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Contribution Start"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:41
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Contribution type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:39
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Contributions for %{name}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:159
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Current"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:175
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Example: Member Contribution View"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:361
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Joining year - reduced to 0"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:116
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Mark as Paid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:120
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Mark as Suspended"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:124
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Mark as Unpaid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:26
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Member Contributions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:43
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Member since"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:331
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Paid via bank transfer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:178
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "See how the contribution periods will be displayed for an individual member. This example shows Maria Weber with multiple contribution periods."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:139
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:261
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Suspended"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:252
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Unpaid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:185
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "View Example Member"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:92
|
||||||
|
#, 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/contribution_period_live/show.ex:70
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Open Contributions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:204
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Reopen"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:195
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Suspend"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:229
|
||||||
|
#: lib/mv_web/live/contribution_settings_live.ex:201
|
||||||
|
#: lib/mv_web/live/contribution_type_live/index.ex:99
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "This page is not functional and only displays the planned features."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:136
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Time Period"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:66
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Total Contributions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/contribution_period_live/show.ex:98
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Why are not all contribution types shown?"
|
||||||
|
msgstr ""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
defmodule Mv.Repo.Migrations.RemoveBirthDateFromMembers do
|
||||||
|
@moduledoc """
|
||||||
|
Removes the birth_date column from the members table.
|
||||||
|
|
||||||
|
The birth_date field has been removed from the application because most users
|
||||||
|
don't record birthday data. Users who need this can use a custom field instead.
|
||||||
|
|
||||||
|
This migration also updates the search_vector trigger to remove birth_date.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
# Update the trigger function to remove birth_date from search_vector
|
||||||
|
execute("""
|
||||||
|
CREATE OR REPLACE FUNCTION members_search_vector_trigger() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.search_vector :=
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.first_name, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.last_name, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.email, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.phone_number, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.join_date::text, '')), 'D') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.exit_date::text, '')), 'D') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.notes, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.city, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.street, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.house_number::text, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.postal_code::text, '')), 'C');
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Remove the birth_date column
|
||||||
|
alter table(:members) do
|
||||||
|
remove :birth_date
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
# Add the birth_date column back
|
||||||
|
alter table(:members) do
|
||||||
|
add :birth_date, :date
|
||||||
|
end
|
||||||
|
|
||||||
|
# Restore the trigger function with birth_date
|
||||||
|
execute("""
|
||||||
|
CREATE OR REPLACE FUNCTION members_search_vector_trigger() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.search_vector :=
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.first_name, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.last_name, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.email, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.birth_date::text, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.phone_number, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.join_date::text, '')), 'D') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.exit_date::text, '')), 'D') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.notes, '')), 'B') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.city, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.street, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.house_number::text, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', coalesce(NEW.postal_code::text, '')), 'C');
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -112,7 +112,6 @@ for member_attrs <- [
|
||||||
first_name: "Hans",
|
first_name: "Hans",
|
||||||
last_name: "Müller",
|
last_name: "Müller",
|
||||||
email: "hans.mueller@example.de",
|
email: "hans.mueller@example.de",
|
||||||
birth_date: ~D[1985-06-15],
|
|
||||||
join_date: ~D[2023-01-15],
|
join_date: ~D[2023-01-15],
|
||||||
paid: true,
|
paid: true,
|
||||||
phone_number: "+49301234567",
|
phone_number: "+49301234567",
|
||||||
|
|
@ -125,7 +124,6 @@ for member_attrs <- [
|
||||||
first_name: "Greta",
|
first_name: "Greta",
|
||||||
last_name: "Schmidt",
|
last_name: "Schmidt",
|
||||||
email: "greta.schmidt@example.de",
|
email: "greta.schmidt@example.de",
|
||||||
birth_date: ~D[1990-03-22],
|
|
||||||
join_date: ~D[2023-02-01],
|
join_date: ~D[2023-02-01],
|
||||||
paid: false,
|
paid: false,
|
||||||
phone_number: "+49309876543",
|
phone_number: "+49309876543",
|
||||||
|
|
@ -139,7 +137,6 @@ for member_attrs <- [
|
||||||
first_name: "Friedrich",
|
first_name: "Friedrich",
|
||||||
last_name: "Wagner",
|
last_name: "Wagner",
|
||||||
email: "friedrich.wagner@example.de",
|
email: "friedrich.wagner@example.de",
|
||||||
birth_date: ~D[1978-11-08],
|
|
||||||
join_date: ~D[2022-11-10],
|
join_date: ~D[2022-11-10],
|
||||||
paid: true,
|
paid: true,
|
||||||
phone_number: "+49301122334",
|
phone_number: "+49301122334",
|
||||||
|
|
@ -151,7 +148,6 @@ for member_attrs <- [
|
||||||
first_name: "Marianne",
|
first_name: "Marianne",
|
||||||
last_name: "Wagner",
|
last_name: "Wagner",
|
||||||
email: "marianne.wagner@example.de",
|
email: "marianne.wagner@example.de",
|
||||||
birth_date: ~D[1978-11-08],
|
|
||||||
join_date: ~D[2022-11-10],
|
join_date: ~D[2022-11-10],
|
||||||
paid: true,
|
paid: true,
|
||||||
phone_number: "+49301122334",
|
phone_number: "+49301122334",
|
||||||
|
|
@ -186,7 +182,6 @@ linked_members = [
|
||||||
first_name: "Maria",
|
first_name: "Maria",
|
||||||
last_name: "Weber",
|
last_name: "Weber",
|
||||||
email: "maria.weber@example.de",
|
email: "maria.weber@example.de",
|
||||||
birth_date: ~D[1992-07-14],
|
|
||||||
join_date: ~D[2023-03-15],
|
join_date: ~D[2023-03-15],
|
||||||
paid: true,
|
paid: true,
|
||||||
phone_number: "+49301357924",
|
phone_number: "+49301357924",
|
||||||
|
|
@ -202,7 +197,6 @@ linked_members = [
|
||||||
first_name: "Thomas",
|
first_name: "Thomas",
|
||||||
last_name: "Klein",
|
last_name: "Klein",
|
||||||
email: "thomas.klein@example.de",
|
email: "thomas.klein@example.de",
|
||||||
birth_date: ~D[1988-12-03],
|
|
||||||
join_date: ~D[2023-04-01],
|
join_date: ~D[2023-04-01],
|
||||||
paid: false,
|
paid: false,
|
||||||
phone_number: "+49302468135",
|
phone_number: "+49302468135",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ defmodule Mv.Membership.MemberTest do
|
||||||
@valid_attrs %{
|
@valid_attrs %{
|
||||||
first_name: "John",
|
first_name: "John",
|
||||||
last_name: "Doe",
|
last_name: "Doe",
|
||||||
birth_date: ~D[1990-01-01],
|
|
||||||
paid: true,
|
paid: true,
|
||||||
email: "john@example.com",
|
email: "john@example.com",
|
||||||
phone_number: "+49123456789",
|
phone_number: "+49123456789",
|
||||||
|
|
@ -43,12 +42,6 @@ defmodule Mv.Membership.MemberTest do
|
||||||
assert error_message(errors, :email) =~ "is not a valid email"
|
assert error_message(errors, :email) =~ "is not a valid email"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Birth date is optional but must not be in the future" do
|
|
||||||
attrs = Map.put(@valid_attrs, :birth_date, Date.utc_today() |> Date.add(1))
|
|
||||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
|
||||||
assert error_message(errors, :birth_date) =~ "cannot be in the future"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "Paid is optional but must be boolean if specified" do
|
test "Paid is optional but must be boolean if specified" do
|
||||||
attrs = Map.put(@valid_attrs, :paid, nil)
|
attrs = Map.put(@valid_attrs, :paid, nil)
|
||||||
attrs2 = Map.put(@valid_attrs, :paid, "yes")
|
attrs2 = Map.put(@valid_attrs, :paid, "yes")
|
||||||
|
|
|
||||||
183
test/mv_web/components/payment_filter_component_test.exs
Normal file
183
test/mv_web/components/payment_filter_component_test.exs
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
defmodule MvWeb.Components.PaymentFilterComponentTest do
|
||||||
|
@moduledoc """
|
||||||
|
Unit tests for the PaymentFilterComponent.
|
||||||
|
|
||||||
|
Tests cover:
|
||||||
|
- Rendering in all 3 filter states (nil, :paid, :not_paid)
|
||||||
|
- Event emission when selecting options
|
||||||
|
- ARIA attributes for accessibility
|
||||||
|
- Dropdown open/close behavior
|
||||||
|
"""
|
||||||
|
# async: false to prevent PostgreSQL deadlocks when running LiveView tests against DB
|
||||||
|
use MvWeb.ConnCase, async: false
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
|
describe "rendering" do
|
||||||
|
test "renders with no filter active (nil)", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Should show "All" text and no badge
|
||||||
|
assert has_element?(view, "#payment-filter")
|
||||||
|
refute has_element?(view, "#payment-filter .badge")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders with paid filter active", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members?paid_filter=paid")
|
||||||
|
|
||||||
|
# Should show badge when filter is active
|
||||||
|
assert has_element?(view, "#payment-filter .badge")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders with not_paid filter active", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members?paid_filter=not_paid")
|
||||||
|
|
||||||
|
# Should show badge when filter is active
|
||||||
|
assert has_element?(view, "#payment-filter .badge")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "dropdown behavior" do
|
||||||
|
test "dropdown opens on button click", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Initially dropdown is closed
|
||||||
|
refute has_element?(view, "#payment-filter ul[role='menu']")
|
||||||
|
|
||||||
|
# Click to open
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Dropdown should be visible
|
||||||
|
assert has_element?(view, "#payment-filter ul[role='menu']")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "dropdown closes after selecting an option", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Open dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
assert has_element?(view, "#payment-filter ul[role='menu']")
|
||||||
|
|
||||||
|
# Select an option - this should close the dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[phx-value-filter='paid']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# After selection, dropdown should be closed
|
||||||
|
# Note: The dropdown closes via assign, which is reflected in the next render
|
||||||
|
refute has_element?(view, "#payment-filter ul[role='menu']")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "filter selection" do
|
||||||
|
test "selecting 'All' clears the filter and updates URL", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members?paid_filter=paid")
|
||||||
|
|
||||||
|
# Open dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Select "All" option
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[phx-value-filter='']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# URL should not contain paid_filter param - wait for patch
|
||||||
|
assert_patch(view)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "selecting 'Paid' sets the filter and updates URL", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Open dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Select "Paid" option
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[phx-value-filter='paid']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Wait for patch and check URL contains paid_filter=paid
|
||||||
|
path = assert_patch(view)
|
||||||
|
assert path =~ "paid_filter=paid"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "selecting 'Not paid' sets the filter and updates URL", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Open dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Select "Not paid" option
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[phx-value-filter='not_paid']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Wait for patch and check URL contains paid_filter=not_paid
|
||||||
|
path = assert_patch(view)
|
||||||
|
assert path =~ "paid_filter=not_paid"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "accessibility" do
|
||||||
|
test "has correct ARIA attributes", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Main button should have aria-haspopup and aria-expanded
|
||||||
|
assert html =~ ~s(aria-haspopup="true")
|
||||||
|
assert html =~ ~s(aria-expanded="false")
|
||||||
|
assert html =~ ~s(aria-label=)
|
||||||
|
|
||||||
|
# Open dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
html = render(view)
|
||||||
|
|
||||||
|
# Check aria-expanded is now true
|
||||||
|
assert html =~ ~s(aria-expanded="true")
|
||||||
|
|
||||||
|
# Menu should have role="menu"
|
||||||
|
assert html =~ ~s(role="menu")
|
||||||
|
|
||||||
|
# Options should have role="menuitemradio"
|
||||||
|
assert html =~ ~s(role="menuitemradio")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has aria-checked on selected option", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members?paid_filter=paid")
|
||||||
|
|
||||||
|
# Open dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
html = render(view)
|
||||||
|
|
||||||
|
# "Paid" option should have aria-checked="true"
|
||||||
|
# Check both possible orderings of attributes
|
||||||
|
assert html =~ "aria-checked=\"true\"" and html =~ "phx-value-filter=\"paid\""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -9,7 +9,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
||||||
- Custom field values are correctly formatted for different types
|
- Custom field values are correctly formatted for different types
|
||||||
- Members without custom field values show empty cell or "-"
|
- Members without custom field values show empty cell or "-"
|
||||||
"""
|
"""
|
||||||
use MvWeb.ConnCase, async: true
|
# async: false to prevent PostgreSQL deadlocks when creating members and custom fields
|
||||||
|
use MvWeb.ConnCase, async: false
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -469,4 +469,221 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
assert has_element?(view, "#flash-group")
|
assert has_element?(view, "#flash-group")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "payment filter integration" do
|
||||||
|
setup do
|
||||||
|
# Create members with different payment status
|
||||||
|
# Use unique names that won't appear elsewhere in the HTML
|
||||||
|
{:ok, paid_member} =
|
||||||
|
Mv.Membership.create_member(%{
|
||||||
|
first_name: "Zahler",
|
||||||
|
last_name: "Mitglied",
|
||||||
|
email: "zahler@example.com",
|
||||||
|
paid: true
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, unpaid_member} =
|
||||||
|
Mv.Membership.create_member(%{
|
||||||
|
first_name: "Nichtzahler",
|
||||||
|
last_name: "Mitglied",
|
||||||
|
email: "nichtzahler@example.com",
|
||||||
|
paid: false
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, nil_paid_member} =
|
||||||
|
Mv.Membership.create_member(%{
|
||||||
|
first_name: "Unbestimmt",
|
||||||
|
last_name: "Mitglied",
|
||||||
|
email: "unbestimmt@example.com"
|
||||||
|
# paid is nil by default
|
||||||
|
})
|
||||||
|
|
||||||
|
%{paid_member: paid_member, unpaid_member: unpaid_member, nil_paid_member: nil_paid_member}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filter shows all members when no filter is active", %{
|
||||||
|
conn: conn,
|
||||||
|
paid_member: paid_member,
|
||||||
|
unpaid_member: unpaid_member,
|
||||||
|
nil_paid_member: nil_paid_member
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
assert html =~ paid_member.first_name
|
||||||
|
assert html =~ unpaid_member.first_name
|
||||||
|
assert html =~ nil_paid_member.first_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filter shows only paid members when paid filter is active", %{
|
||||||
|
conn: conn,
|
||||||
|
paid_member: paid_member,
|
||||||
|
unpaid_member: unpaid_member,
|
||||||
|
nil_paid_member: nil_paid_member
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members?paid_filter=paid")
|
||||||
|
|
||||||
|
assert html =~ paid_member.first_name
|
||||||
|
refute html =~ unpaid_member.first_name
|
||||||
|
refute html =~ nil_paid_member.first_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filter shows only unpaid members (including nil) when not_paid filter is active", %{
|
||||||
|
conn: conn,
|
||||||
|
paid_member: paid_member,
|
||||||
|
unpaid_member: unpaid_member,
|
||||||
|
nil_paid_member: nil_paid_member
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members?paid_filter=not_paid")
|
||||||
|
|
||||||
|
refute html =~ paid_member.first_name
|
||||||
|
assert html =~ unpaid_member.first_name
|
||||||
|
assert html =~ nil_paid_member.first_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filter combines with search query (AND)", %{
|
||||||
|
conn: conn,
|
||||||
|
paid_member: paid_member
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members?query=Zahler&paid_filter=paid")
|
||||||
|
|
||||||
|
assert html =~ paid_member.first_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "filter combines with sorting", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
|
||||||
|
{:ok, view, _html} =
|
||||||
|
live(conn, "/members?paid_filter=paid&sort_field=first_name&sort_order=asc")
|
||||||
|
|
||||||
|
# Click on email sort header
|
||||||
|
view
|
||||||
|
|> element("[data-testid='email']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Filter should be preserved in URL
|
||||||
|
path = assert_patch(view)
|
||||||
|
assert path =~ "paid_filter=paid"
|
||||||
|
assert path =~ "sort_field=email"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "URL parameter paid_filter is set when selecting filter", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Open filter dropdown
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[aria-haspopup='true']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Select "Paid" option
|
||||||
|
view
|
||||||
|
|> element("#payment-filter button[phx-value-filter='paid']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
path = assert_patch(view)
|
||||||
|
assert path =~ "paid_filter=paid"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "URL parameter is correctly read on page load", %{
|
||||||
|
conn: conn,
|
||||||
|
paid_member: paid_member
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members?paid_filter=paid")
|
||||||
|
|
||||||
|
# Only paid member should be visible
|
||||||
|
assert html =~ paid_member.first_name
|
||||||
|
# Filter badge should be visible
|
||||||
|
assert html =~ "badge"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invalid URL parameter is ignored", %{
|
||||||
|
conn: conn,
|
||||||
|
paid_member: paid_member,
|
||||||
|
unpaid_member: unpaid_member
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members?paid_filter=invalid_value")
|
||||||
|
|
||||||
|
# All members should be visible (filter not applied)
|
||||||
|
assert html =~ paid_member.first_name
|
||||||
|
assert html =~ unpaid_member.first_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "search maintains filter state", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members?paid_filter=paid")
|
||||||
|
|
||||||
|
# Perform search
|
||||||
|
view
|
||||||
|
|> element("[data-testid='search-input']")
|
||||||
|
|> render_change(%{"query" => "test"})
|
||||||
|
|
||||||
|
# Filter state should be maintained in URL
|
||||||
|
path = assert_patch(view)
|
||||||
|
assert path =~ "paid_filter=paid"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "paid column in table" do
|
||||||
|
setup do
|
||||||
|
{:ok, paid_member} =
|
||||||
|
Mv.Membership.create_member(%{
|
||||||
|
first_name: "Paid",
|
||||||
|
last_name: "Member",
|
||||||
|
email: "paid.column@example.com",
|
||||||
|
paid: true
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, unpaid_member} =
|
||||||
|
Mv.Membership.create_member(%{
|
||||||
|
first_name: "Unpaid",
|
||||||
|
last_name: "Member",
|
||||||
|
email: "unpaid.column@example.com",
|
||||||
|
paid: false
|
||||||
|
})
|
||||||
|
|
||||||
|
%{paid_member: paid_member, unpaid_member: unpaid_member}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "paid column shows green badge for paid members", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Check for success badge (green)
|
||||||
|
assert html =~ "badge-success"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "paid column shows red badge for unpaid members", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Check for error badge (red)
|
||||||
|
assert html =~ "badge-error"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "paid column shows 'Yes' for paid members", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
Gettext.put_locale(MvWeb.Gettext, "en")
|
||||||
|
{:ok, _view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# The table should contain "Yes" text inside badge
|
||||||
|
assert html =~ "badge-success"
|
||||||
|
assert html =~ "Yes"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "paid column shows 'No' for unpaid members", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
Gettext.put_locale(MvWeb.Gettext, "en")
|
||||||
|
{:ok, _view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# The table should contain "No" text inside badge
|
||||||
|
assert html =~ "badge-error"
|
||||||
|
assert html =~ "No"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue