diff --git a/docs/contributions-architecture.md b/docs/contributions-architecture.md new file mode 100644 index 0000000..3718a3b --- /dev/null +++ b/docs/contributions-architecture.md @@ -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** + diff --git a/docs/contributions-overview.md b/docs/contributions-overview.md new file mode 100644 index 0000000..e0c4bc8 --- /dev/null +++ b/docs/contributions-overview.md @@ -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 diff --git a/docs/database-schema-readme.md b/docs/database-schema-readme.md index d548b82..1644f2a 100644 --- a/docs/database-schema-readme.md +++ b/docs/database-schema-readme.md @@ -115,7 +115,6 @@ Member (1) → (N) Properties ### Member Constraints - First name and last name required (min 1 char) - Email unique, validated format (5-254 chars) -- Birth date cannot be in future - Join date cannot be in future - Exit date must be after join date - Phone: `+?[0-9\- ]{6,20}` @@ -169,7 +168,7 @@ Member (1) → (N) Properties ### Weighted Fields - **Weight A (highest):** first_name, last_name - **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 ### Usage Example @@ -381,7 +380,7 @@ Install "DBML Language" extension to view/edit DBML files with: - tokens (jti, purpose, extra_data) **Personal Data (GDPR):** -- All member fields (name, email, birth_date, address) +- All member fields (name, email, address) - User email - Token subject diff --git a/docs/database_schema.dbml b/docs/database_schema.dbml index 33c0647..b620830 100644 --- a/docs/database_schema.dbml +++ b/docs/database_schema.dbml @@ -122,7 +122,6 @@ Table members { first_name text [not null, note: 'Member first 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)'] - birth_date date [null, note: 'Date of birth (cannot be in future)'] paid boolean [null, note: 'Payment status flag'] phone_number text [null, note: 'Contact phone number (format: +?[0-9\- ]{6,20})'] join_date date [null, note: 'Date when member joined club (cannot be in future)'] @@ -153,7 +152,7 @@ Table members { **Club Member Master Data** Core entity for membership management containing: - - Personal information (name, birth date, email) + - Personal information (name, email) - Contact details (phone, address) - Membership status (join/exit dates, payment status) - Additional notes @@ -183,7 +182,6 @@ Table members { **Validation Rules:** - first_name, last_name: min 1 character - email: 5-254 characters, valid email format - - birth_date: cannot be in future - join_date: cannot be in future - exit_date: must be after join_date (if both present) - phone_number: matches pattern ^\+?[0-9\- ]{6,20}$ diff --git a/docs/feature-roadmap.md b/docs/feature-roadmap.md index 60432d0..2f86f5e 100644 --- a/docs/feature-roadmap.md +++ b/docs/feature-roadmap.md @@ -100,10 +100,10 @@ **Closed Issues:** - [#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) +- [#161](https://git.local-it.org/local-it/mitgliederverwaltung/issues/161) - Remove birthday field from default configuration (S) - Closed 2025-12-02 **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] -- [#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) **Missing Features:** @@ -187,10 +187,16 @@ **Current State:** - ✅ Basic "paid" boolean field on members +- ✅ **UI Mock-ups for Contribution Types & Settings** (2025-12-02) - ⚠️ No payment tracking **Open Issues:** - [#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:** - ❌ Membership fee configuration diff --git a/lib/membership/member.ex b/lib/membership/member.ex index bcd505e..8d271d7 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -24,7 +24,7 @@ defmodule Mv.Membership.Member do - Email format validation (using EctoCommons.EmailValidator) - Phone number format: international format with 6-20 digits - 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 ## Full-Text Search @@ -284,11 +284,6 @@ defmodule Mv.Membership.Member do 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 validate compare(:join_date, less_than_or_equal_to: &Date.utc_today/0), where: [present(:join_date)], @@ -351,10 +346,6 @@ defmodule Mv.Membership.Member do constraints min_length: 5, max_length: 254 end - attribute :birth_date, :date do - allow_nil? true - end - attribute :paid, :boolean do allow_nil? true end diff --git a/lib/mv/constants.ex b/lib/mv/constants.ex index cd8d3a4..334bcc1 100644 --- a/lib/mv/constants.ex +++ b/lib/mv/constants.ex @@ -7,7 +7,6 @@ defmodule Mv.Constants do :first_name, :last_name, :email, - :birth_date, :paid, :phone_number, :join_date, diff --git a/lib/mv_web/components/layouts/navbar.ex b/lib/mv_web/components/layouts/navbar.ex index 7ff7f25..1c1138e 100644 --- a/lib/mv_web/components/layouts/navbar.ex +++ b/lib/mv_web/components/layouts/navbar.ex @@ -25,6 +25,17 @@ defmodule MvWeb.Layouts.Navbar do
  • <.link navigate="/members">{gettext("Members")}
  • <.link navigate="/custom_fields">{gettext("Custom Fields")}
  • <.link navigate="/users">{gettext("Users")}
  • +
  • +
    + {gettext("Contributions")} + +
    +
  • diff --git a/lib/mv_web/live/components/payment_filter_component.ex b/lib/mv_web/live/components/payment_filter_component.ex new file mode 100644 index 0000000..c9dc731 --- /dev/null +++ b/lib/mv_web/live/components/payment_filter_component.ex @@ -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""" +
    + + + +
    + """ + 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 diff --git a/lib/mv_web/live/contribution_period_live/show.ex b/lib/mv_web/live/contribution_period_live/show.ex new file mode 100644 index 0000000..e1f4b93 --- /dev/null +++ b/lib/mv_web/live/contribution_period_live/show.ex @@ -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""" + + <.mockup_warning /> + + <.header> + {gettext("Contributions for %{name}", name: "#{@member.first_name} #{@member.last_name}")} + <:subtitle> + {gettext("Contribution type")}: + {@member.contribution_type} + · {gettext("Member since")}: {@member.joined_at} + + <:actions> + <.link navigate={~p"/contribution_settings"} class="btn btn-ghost btn-sm"> + <.icon name="hero-arrow-left" class="size-4" /> + {gettext("Back to Settings")} + + + + + <%!-- Member Info Card --%> +
    +
    +
    +
    + {gettext("Email")} +

    {@member.email}

    +
    +
    + {gettext("Contribution Start")} +

    {@member.contribution_start}

    +
    +
    + {gettext("Total Contributions")} +

    {length(@periods)}

    +
    +
    + {gettext("Open Contributions")} +

    + {Enum.count(@periods, &(&1.status == :unpaid))} +

    +
    +
    +
    +
    + + <%!-- Contribution Type Change --%> +
    +
    +
    + {gettext("Change Contribution Type")}: + + + <.icon name="hero-question-mark-circle" class="size-4 inline" /> + {gettext("Why are not all contribution types shown?")} + +
    +
    +
    + + <%!-- Bulk Actions --%> +
    + + {ngettext( + "%{count} period selected", + "%{count} periods selected", + MapSet.size(@selected_periods), + count: MapSet.size(@selected_periods) + )} + + + + +
    + + <%!-- Periods Table --%> +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + + {gettext("Time Period")}{gettext("Interval")}{gettext("Amount")}{gettext("Status")}{gettext("Notes")}{gettext("Actions")}
    + + +
    + {period.period_start} – {period.period_end} +
    +
    + {gettext("Current")} +
    +
    + {format_interval(period.interval)} + + {format_currency(period.amount)} + + <.status_badge status={period.status} /> + + + {period.notes} + + + +
    + <.link + href="#" + class={[ + "cursor-not-allowed", + if(period.status == :paid, do: "invisible", else: "opacity-50") + ]} + > + {gettext("Paid")} + + <.link + href="#" + class={[ + "cursor-not-allowed", + if(period.status == :suspended, do: "invisible", else: "opacity-50") + ]} + > + {gettext("Suspend")} + + <.link + href="#" + class={[ + "cursor-not-allowed", + if(period.status != :paid, do: "invisible", else: "opacity-50") + ]} + > + {gettext("Reopen")} + + <.link href="#" class="opacity-50 cursor-not-allowed"> + {gettext("Note")} + +
    +
    +
    + + <.navigation_card /> +
    + """ + end + + # Mock-up warning banner component - subtle orange style + defp mockup_warning(assigns) do + ~H""" +
    + <.icon name="hero-exclamation-triangle" class="size-5 shrink-0" /> +
    + {gettext("Preview Mockup")} + + – {gettext("This page is not functional and only displays the planned features.")} + +
    +
    + """ + end + + # Status badge component + attr :status, :atom, required: true + + defp status_badge(%{status: :paid} = assigns) do + ~H""" + + <.icon name="hero-check-circle-mini" class="size-3" /> + {gettext("Paid")} + + """ + end + + defp status_badge(%{status: :unpaid} = assigns) do + ~H""" + + <.icon name="hero-x-circle-mini" class="size-3" /> + {gettext("Unpaid")} + + """ + end + + defp status_badge(%{status: :suspended} = assigns) do + ~H""" + + <.icon name="hero-pause-circle-mini" class="size-3" /> + {gettext("Suspended")} + + """ + end + + # Navigation card + defp navigation_card(assigns) do + ~H""" +
    +
    +

    + <.icon name="hero-arrow-right-circle" class="size-5" /> + {gettext("Related Pages")} +

    +
    + <.link navigate={~p"/contribution_types"} class="btn btn-outline btn-sm"> + <.icon name="hero-tag" class="size-4" /> + {gettext("Contribution Types")} + + <.link navigate={~p"/contribution_settings"} class="btn btn-outline btn-sm"> + <.icon name="hero-cog-6-tooth" class="size-4" /> + {gettext("Contribution Settings")} + + <.link navigate={~p"/members"} class="btn btn-outline btn-sm"> + <.icon name="hero-users" class="size-4" /> + {gettext("Members")} + +
    +
    +
    + """ + 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 diff --git a/lib/mv_web/live/contribution_settings_live.ex b/lib/mv_web/live/contribution_settings_live.ex new file mode 100644 index 0000000..3543457 --- /dev/null +++ b/lib/mv_web/live/contribution_settings_live.ex @@ -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""" + + <.mockup_warning /> + + <.header> + {gettext("Contribution Settings")} + <:subtitle> + {gettext("Configure global settings for membership contributions.")} + + + +
    + <%!-- Settings Form --%> +
    +
    +

    + <.icon name="hero-cog-6-tooth" class="size-5" /> + {gettext("Global Settings")} +

    + +
    + <%!-- Default Contribution Type --%> +
    + + +

    + {gettext( + "This contribution type is automatically assigned to all new members. Can be changed individually per member." + )} +

    +
    + + <%!-- Include Joining Period --%> +
    + +
    +

    + {gettext("When active: Members pay from the period of their joining.")} +

    +

    + {gettext("When inactive: Members pay from the next full period after joining.")} +

    +
    +
    + +
    + + +
    +
    +
    + + <%!-- Examples Card --%> +
    +
    +

    + <.icon name="hero-light-bulb" class="size-5" /> + {gettext("Examples")} +

    + + <.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")} + /> + +
    + + <.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")} + /> + +
    + + <.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")} + /> + +
    + + <.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")} + /> +
    +
    +
    + + <.example_member_card /> + + <.navigation_card /> +
    + """ + end + + # Example member card with link to period view + defp example_member_card(assigns) do + ~H""" +
    +
    +

    + <.icon name="hero-user" class="size-5" /> + {gettext("Example: Member Contribution View")} +

    +

    + {gettext( + "See how the contribution periods will be displayed for an individual member. This example shows Maria Weber with multiple contribution periods." + )} +

    +
    + <.link navigate={~p"/contributions/member/example"} class="btn btn-primary btn-sm"> + <.icon name="hero-eye" class="size-4" /> + {gettext("View Example Member")} + +
    +
    +
    + """ + end + + # Mock-up warning banner component - subtle orange style + defp mockup_warning(assigns) do + ~H""" +
    + <.icon name="hero-exclamation-triangle" class="size-5 shrink-0" /> +
    + {gettext("Preview Mockup")} + + – {gettext("This page is not functional and only displays the planned features.")} + +
    +
    + """ + 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""" +
    +

    {@title}

    +
    +

    + {gettext("Joining date")}: + {@joining_date} +

    +

    + {gettext("Contribution start")}: + {@start_date} +

    +

    + {gettext("Generated periods")}: + + {Enum.join(@periods, ", ")} + +

    +
    +

    → {@note}

    +
    + """ + end + + # Navigation card to other contribution pages + defp navigation_card(assigns) do + ~H""" +
    +
    +

    + <.icon name="hero-arrow-right-circle" class="size-5" /> + {gettext("Related Pages")} +

    +
    + <.link navigate={~p"/contribution_types"} class="btn btn-outline btn-sm"> + <.icon name="hero-tag" class="size-4" /> + {gettext("Contribution Types")} + + <.link navigate={~p"/settings"} class="btn btn-outline btn-sm"> + <.icon name="hero-cog-6-tooth" class="size-4" /> + {gettext("Club Settings")} + +
    +
    +
    + """ + 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 diff --git a/lib/mv_web/live/contribution_type_live/index.ex b/lib/mv_web/live/contribution_type_live/index.ex new file mode 100644 index 0000000..9a7b602 --- /dev/null +++ b/lib/mv_web/live/contribution_type_live/index.ex @@ -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""" + + <.mockup_warning /> + + <.header> + {gettext("Contribution Types")} + <:subtitle> + {gettext("Manage contribution types for membership fees.")} + + <:actions> + + + + + <.table id="contribution_types" rows={@contribution_types} row_id={fn ct -> "ct-#{ct.id}" end}> + <:col :let={ct} label={gettext("Name")}> + {ct.name} +

    {ct.description}

    + + + <:col :let={ct} label={gettext("Amount")}> + {format_currency(ct.amount)} + + + <:col :let={ct} label={gettext("Interval")}> + {format_interval(ct.interval)} + + + <:col :let={ct} label={gettext("Members")}> + {ct.member_count} + + + <:action :let={_ct}> + + + + <:action :let={ct}> + + + + + <.info_card /> +
    + """ + end + + # Mock-up warning banner component - subtle orange style + defp mockup_warning(assigns) do + ~H""" +
    + <.icon name="hero-exclamation-triangle" class="size-5 shrink-0" /> +
    + {gettext("Preview Mockup")} + + – {gettext("This page is not functional and only displays the planned features.")} + +
    +
    + """ + end + + # Info card explaining the contribution type concept + defp info_card(assigns) do + ~H""" +
    +
    +

    + <.icon name="hero-information-circle" class="size-5" /> + {gettext("About Contribution Types")} +

    +
    +

    + {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." + )} +

    +
      +
    • + {gettext("Name & Amount")} + - {gettext("Can be changed at any time. Amount changes affect future periods only.")} +
    • +
    • + {gettext("Interval")} + - {gettext( + "Fixed after creation. Members can only switch between types with the same interval." + )} +
    • +
    • + {gettext("Deletion")} + - {gettext("Only possible if no members are assigned to this type.")} +
    • +
    +
    +
    +
    + """ + 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 diff --git a/lib/mv_web/live/member_live/form.ex b/lib/mv_web/live/member_live/form.ex index 8d8863f..5370154 100644 --- a/lib/mv_web/live/member_live/form.ex +++ b/lib/mv_web/live/member_live/form.ex @@ -14,7 +14,7 @@ defmodule MvWeb.MemberLive.Form do - first_name, last_name, email **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 - paid status - notes @@ -45,7 +45,6 @@ defmodule MvWeb.MemberLive.Form do <.input field={@form[:first_name]} label={gettext("First 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[:birth_date]} label={gettext("Birth Date")} type="date" /> <.input field={@form[:paid]} label={gettext("Paid")} type="checkbox" /> <.input field={@form[:phone_number]} label={gettext("Phone Number")} /> <.input field={@form[:join_date]} label={gettext("Join Date")} type="date" /> diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 4d444b9..3d30d76 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -46,7 +46,7 @@ defmodule MvWeb.MemberLive.Index do Initializes the LiveView state. 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 def mount(_params, _session, socket) do @@ -74,6 +74,7 @@ defmodule MvWeb.MemberLive.Index do |> assign(:query, "") |> assign_new(:sort_field, fn -> :first_name end) |> assign_new(:sort_order, fn -> :asc end) + |> assign(:paid_filter, nil) |> assign(:selected_members, MapSet.new()) |> assign(:custom_fields_visible, custom_fields_visible) |> assign(:member_fields_visible, get_visible_member_fields(settings)) @@ -207,17 +208,17 @@ defmodule MvWeb.MemberLive.Index do @impl true 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_sort_query = socket.assigns.sort_order # Build the URL with queries - query_params = %{ - "query" => q, - "sort_field" => existing_field_query, - "sort_order" => existing_sort_query - } + query_params = + build_query_params(q, existing_field_query, existing_sort_query, socket.assigns.paid_filter) # Set the new path with params new_path = ~p"/members?#{query_params}" @@ -230,13 +231,38 @@ defmodule MvWeb.MemberLive.Index do )} 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 # ----------------------------------------------------------------- @doc """ 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 browser back/forward navigation. """ @@ -246,7 +272,9 @@ defmodule MvWeb.MemberLive.Index do socket |> maybe_update_search(params) |> maybe_update_sort(params) - |> load_members(params["query"]) + |> maybe_update_paid_filter(params) + |> assign(:query, params["query"]) + |> load_members() |> prepare_dynamic_cols() {:noreply, socket} @@ -337,11 +365,13 @@ defmodule MvWeb.MemberLive.Index do field end - query_params = %{ - "query" => socket.assigns.query, - "sort_field" => field_str, - "sort_order" => Atom.to_string(order) - } + query_params = + build_query_params( + socket.assigns.query, + field_str, + Atom.to_string(order), + socket.assigns.paid_filter + ) new_path = ~p"/members?#{query_params}" @@ -352,13 +382,45 @@ defmodule MvWeb.MemberLive.Index do )} 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: # 1. Builds base query with selected fields # 2. Loads custom field values for visible custom fields (filtered at database level) # 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: # - 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). # # Returns the socket with `:members` assigned. - defp load_members(socket, search_query) do + defp load_members(socket) do + search_query = socket.assigns.query + query = Mv.Membership.Member |> Ash.Query.new() @@ -383,6 +447,9 @@ defmodule MvWeb.MemberLive.Index do # Apply the search filter first 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 # For custom fields, we sort after loading {query, sort_after_load} = @@ -457,6 +524,24 @@ defmodule MvWeb.MemberLive.Index do 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 defp toggle_order(:asc), do: :desc defp toggle_order(:desc), do: :asc @@ -747,6 +832,29 @@ defmodule MvWeb.MemberLive.Index do socket 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 # ------------------------------------------------------------- diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index 55b0a20..58e22b6 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -26,12 +26,20 @@ - <.live_component - module={MvWeb.Components.SearchBarComponent} - id="search-bar" - query={@query} - placeholder={gettext("Search...")} - /> +
    + <.live_component + module={MvWeb.Components.SearchBarComponent} + id="search-bar" + query={@query} + placeholder={gettext("Search...")} + /> + <.live_component + module={MvWeb.Components.PaymentFilterComponent} + id="payment-filter" + paid_filter={@paid_filter} + member_count={length(@members)} + /> +
    <.table id="members" @@ -213,6 +221,14 @@ > {member.join_date} + <:col :let={member} label={gettext("Paid")}> + + {if member.paid == true, do: gettext("Yes"), else: gettext("No")} + + <:action :let={member}>
    <.link navigate={~p"/members/#{member}"}>{gettext("Show")} diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex index 7ec24fa..de46a3a 100644 --- a/lib/mv_web/live/member_live/show.ex +++ b/lib/mv_web/live/member_live/show.ex @@ -10,7 +10,7 @@ defmodule MvWeb.MemberLive.Show do - Return to member list ## Displayed Information - - Basic: name, email, dates (birth, join, exit) + - Basic: name, email, dates (join, exit) - Contact: phone number - Address: street, house number, postal code, city - Status: paid flag @@ -48,7 +48,6 @@ defmodule MvWeb.MemberLive.Show do <:item title={gettext("First Name")}>{@member.first_name} <:item title={gettext("Last Name")}>{@member.last_name} <:item title={gettext("Email")}>{@member.email} - <:item title={gettext("Birth Date")}>{@member.birth_date} <:item title={gettext("Paid")}> {if @member.paid, do: gettext("Yes"), else: gettext("No")} diff --git a/lib/mv_web/router.ex b/lib/mv_web/router.ex index 09a2792..c574e17 100644 --- a/lib/mv_web/router.ex +++ b/lib/mv_web/router.ex @@ -75,6 +75,11 @@ defmodule MvWeb.Router do 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 end diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 68afafc..f7dd49e 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -11,11 +11,12 @@ msgstr "" "Language: en\n" #: lib/mv_web/components/core_components.ex:386 +#: lib/mv_web/live/contribution_period_live/show.ex:141 #, elixir-autogen, elixir-format msgid "Actions" 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 #, elixir-autogen, elixir-format msgid "Are you sure?" @@ -27,20 +28,22 @@ msgstr "Bist du sicher?" msgid "Attempting to reconnect" msgstr "Verbindung wird wiederhergestellt" -#: lib/mv_web/live/member_live/form.ex:54 -#: lib/mv_web/live/member_live/index.html.heex:171 -#: lib/mv_web/live/member_live/show.ex:59 +#: lib/mv_web/live/member_live/form.ex:53 +#: lib/mv_web/live/member_live/index.html.heex:179 +#: lib/mv_web/live/member_live/show.ex:58 #, elixir-autogen, elixir-format msgid "City" 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 #, elixir-autogen, elixir-format msgid "Delete" 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/index.html.heex:66 #, elixir-autogen, elixir-format @@ -48,13 +51,14 @@ msgid "Edit" msgstr "Bearbeite" #: 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 msgid "Edit Member" 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/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/user_live/form.ex:46 #: lib/mv_web/live/user_live/index.html.heex:44 @@ -69,9 +73,9 @@ msgstr "E-Mail" msgid "First Name" msgstr "Vorname" -#: lib/mv_web/live/member_live/form.ex:51 -#: lib/mv_web/live/member_live/index.html.heex:207 -#: lib/mv_web/live/member_live/show.ex:56 +#: lib/mv_web/live/member_live/form.ex:50 +#: lib/mv_web/live/member_live/index.html.heex:215 +#: lib/mv_web/live/member_live/show.ex:55 #, elixir-autogen, elixir-format msgid "Join Date" msgstr "Beitrittsdatum" @@ -87,7 +91,7 @@ msgstr "Nachname" msgid "New Member" 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 #, elixir-autogen, elixir-format msgid "Show" @@ -108,52 +112,52 @@ msgstr "Keine Internetverbindung gefunden" msgid "close" msgstr "schließen" -#: lib/mv_web/live/member_live/form.ex:48 -#: lib/mv_web/live/member_live/show.ex:51 -#, elixir-autogen, elixir-format -msgid "Birth Date" -msgstr "Geburtsdatum" - -#: lib/mv_web/live/member_live/form.ex:52 -#: lib/mv_web/live/member_live/show.ex:57 +#: lib/mv_web/live/member_live/form.ex:51 +#: lib/mv_web/live/member_live/show.ex:56 #, elixir-autogen, elixir-format msgid "Exit Date" msgstr "Austrittsdatum" -#: lib/mv_web/live/member_live/form.ex:56 -#: lib/mv_web/live/member_live/index.html.heex:135 -#: lib/mv_web/live/member_live/show.ex:61 +#: lib/mv_web/live/member_live/form.ex:55 +#: lib/mv_web/live/member_live/index.html.heex:143 +#: lib/mv_web/live/member_live/show.ex:60 #, elixir-autogen, elixir-format msgid "House Number" msgstr "Hausnummer" -#: lib/mv_web/live/member_live/form.ex:53 -#: lib/mv_web/live/member_live/show.ex:58 +#: lib/mv_web/live/contribution_period_live/show.ex:140 +#: lib/mv_web/live/member_live/form.ex:52 +#: lib/mv_web/live/member_live/show.ex:57 #, elixir-autogen, elixir-format msgid "Notes" msgstr "Notizen" -#: lib/mv_web/live/member_live/form.ex:49 -#: lib/mv_web/live/member_live/show.ex:52 +#: lib/mv_web/live/components/payment_filter_component.ex:94 +#: 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 msgid "Paid" msgstr "Bezahlt" -#: lib/mv_web/live/member_live/form.ex:50 -#: lib/mv_web/live/member_live/index.html.heex:189 -#: lib/mv_web/live/member_live/show.ex:55 +#: lib/mv_web/live/member_live/form.ex:49 +#: lib/mv_web/live/member_live/index.html.heex:197 +#: lib/mv_web/live/member_live/show.ex:54 #, elixir-autogen, elixir-format msgid "Phone Number" msgstr "Telefonnummer" -#: lib/mv_web/live/member_live/form.ex:57 -#: lib/mv_web/live/member_live/index.html.heex:153 -#: lib/mv_web/live/member_live/show.ex:62 +#: lib/mv_web/live/member_live/form.ex:56 +#: lib/mv_web/live/member_live/index.html.heex:161 +#: lib/mv_web/live/member_live/show.ex:61 #, elixir-autogen, elixir-format msgid "Postal Code" msgstr "Postleitzahl" -#: lib/mv_web/live/member_live/form.ex:80 +#: lib/mv_web/live/member_live/form.ex:79 #, elixir-autogen, elixir-format msgid "Save Member" 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_value_live/form.ex:74 #: 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 #, elixir-autogen, elixir-format msgid "Saving..." msgstr "Speichern..." -#: lib/mv_web/live/member_live/form.ex:55 -#: lib/mv_web/live/member_live/index.html.heex:117 -#: lib/mv_web/live/member_live/show.ex:60 +#: lib/mv_web/live/member_live/form.ex:54 +#: lib/mv_web/live/member_live/index.html.heex:125 +#: lib/mv_web/live/member_live/show.ex:59 #, elixir-autogen, elixir-format msgid "Street" msgstr "Straße" @@ -179,13 +183,14 @@ msgstr "Straße" msgid "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/show.ex:53 +#: lib/mv_web/live/member_live/show.ex:52 #, elixir-autogen, elixir-format msgid "No" 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 msgid "Show Member" msgstr "Mitglied anzeigen" @@ -195,22 +200,23 @@ msgstr "Mitglied anzeigen" msgid "This is a member record from your database." 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/show.ex:53 +#: lib/mv_web/live/member_live/show.ex:52 #, elixir-autogen, elixir-format msgid "Yes" msgstr "Ja" #: 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/member_live/form.ex:138 +#: lib/mv_web/live/member_live/form.ex:137 #, elixir-autogen, elixir-format msgid "create" msgstr "erstellt" #: 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/member_live/form.ex:139 +#: lib/mv_web/live/member_live/form.ex:138 #, elixir-autogen, elixir-format msgid "update" msgstr "aktualisiert" @@ -220,7 +226,7 @@ msgstr "aktualisiert" msgid "Incorrect email or password" 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 msgid "Member %{action} successfully" 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/index.ex:120 #: 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 #, elixir-autogen, elixir-format msgid "Cancel" @@ -289,7 +295,7 @@ msgstr "ID" msgid "Immutable" msgstr "Unveränderlich" -#: lib/mv_web/components/layouts/navbar.ex:102 +#: lib/mv_web/components/layouts/navbar.ex:113 #, elixir-autogen, elixir-format msgid "Logout" msgstr "Abmelden" @@ -306,12 +312,15 @@ msgid "Member" msgstr "Mitglied" #: 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.html.heex:3 #, elixir-autogen, elixir-format msgid "Members" msgstr "Mitglieder" +#: lib/mv_web/live/contribution_type_live/index.ex:48 #: lib/mv_web/live/custom_field_live/form.ex:51 #, elixir-autogen, elixir-format msgid "Name" @@ -332,6 +341,7 @@ msgstr "Nicht aktiviert" msgid "Not set" 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:115 #: lib/mv_web/live/user_live/form.ex:224 @@ -350,7 +360,7 @@ msgstr "OIDC ID" msgid "Password Authentication" msgstr "Passwort-Authentifizierung" -#: lib/mv_web/components/layouts/navbar.ex:95 +#: lib/mv_web/components/layouts/navbar.ex:106 #, elixir-autogen, elixir-format msgid "Profil" msgstr "Profil" @@ -360,17 +370,17 @@ msgstr "Profil" msgid "Required" 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 msgid "Select all members" 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 msgid "Select member" msgstr "Mitglied auswählen" -#: lib/mv_web/components/layouts/navbar.ex:99 +#: lib/mv_web/components/layouts/navbar.ex:110 #, elixir-autogen, elixir-format msgid "Settings" msgstr "Einstellungen" @@ -510,7 +520,7 @@ msgstr "Benutzer*in wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen msgid "Linked Member" 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 msgid "Linked User" msgstr "Verknüpfte*r Benutzer*in" @@ -521,7 +531,7 @@ msgstr "Verknüpfte*r Benutzer*in" msgid "No member linked" 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 msgid "No user linked" msgstr "Keine*r Benutzer*in verknüpft" @@ -538,20 +548,20 @@ msgstr "Zurück zur Mitgliederliste" msgid "Back to users list" msgstr "Zurück zur Benutzer*innen-Liste" -#: lib/mv_web/components/layouts/navbar.ex:33 -#: lib/mv_web/components/layouts/navbar.ex:39 +#: lib/mv_web/components/layouts/navbar.ex:44 +#: lib/mv_web/components/layouts/navbar.ex:50 #, elixir-autogen, elixir-format msgid "Select language" msgstr "Sprache auswählen" -#: lib/mv_web/components/layouts/navbar.ex:46 -#: lib/mv_web/components/layouts/navbar.ex:66 +#: lib/mv_web/components/layouts/navbar.ex:57 +#: lib/mv_web/components/layouts/navbar.ex:77 #, elixir-autogen, elixir-format msgid "Toggle dark mode" msgstr "Dunklen Modus umschalten" #: 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 msgid "Search..." msgstr "Suchen..." @@ -567,7 +577,7 @@ msgstr "Benutzer*innen" msgid "Click to sort" 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 msgid "First name" 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" msgstr "Wähle ein Benutzerdefiniertes Feld" -#: lib/mv_web/live/member_live/form.ex:59 -#: lib/mv_web/live/member_live/show.ex:78 +#: lib/mv_web/live/member_live/form.ex:58 +#: lib/mv_web/live/member_live/show.ex:77 #, elixir-autogen, elixir-format msgid "Custom Field Values" msgstr "Benutzerdefinierte Feldwerte" @@ -706,6 +716,7 @@ msgstr "In der Mitglieder-Übersicht anzeigen" msgid "Association Name" 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:41 #, elixir-autogen, elixir-format, fuzzy @@ -717,6 +728,7 @@ msgstr "Vereinsdaten" msgid "Manage global settings for the association." 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 #, elixir-autogen, elixir-format, fuzzy msgid "Save Settings" @@ -777,7 +789,7 @@ msgstr "Mitglied entverknüpfen" msgid "Unlinking scheduled" 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 msgid "Copied %{count} email address 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" 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 msgid "No email addresses found" 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 msgid "No members selected" msgstr "Keine Mitglieder ausgewählt" @@ -814,7 +826,7 @@ msgstr "E-Mail-Programm mit BCC-Empfänger*innen öffnen" msgid "Open in email program" 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 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" @@ -831,3 +843,460 @@ msgstr "Felder, die mit einem Sternchen (*) markiert sind, dürfen nicht leer bl #, elixir-autogen, elixir-format, fuzzy msgid "This field cannot be empty" 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?" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 3dd41b5..fb309dd 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -12,11 +12,12 @@ msgid "" msgstr "" #: lib/mv_web/components/core_components.ex:386 +#: lib/mv_web/live/contribution_period_live/show.ex:141 #, elixir-autogen, elixir-format msgid "Actions" 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 #, elixir-autogen, elixir-format msgid "Are you sure?" @@ -28,20 +29,22 @@ msgstr "" msgid "Attempting to reconnect" msgstr "" -#: lib/mv_web/live/member_live/form.ex:54 -#: lib/mv_web/live/member_live/index.html.heex:171 -#: lib/mv_web/live/member_live/show.ex:59 +#: lib/mv_web/live/member_live/form.ex:53 +#: lib/mv_web/live/member_live/index.html.heex:179 +#: lib/mv_web/live/member_live/show.ex:58 #, elixir-autogen, elixir-format msgid "City" 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 #, elixir-autogen, elixir-format msgid "Delete" 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/index.html.heex:66 #, elixir-autogen, elixir-format @@ -49,13 +52,14 @@ msgid "Edit" msgstr "" #: 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 msgid "Edit Member" 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/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/user_live/form.ex:46 #: lib/mv_web/live/user_live/index.html.heex:44 @@ -70,9 +74,9 @@ msgstr "" msgid "First Name" msgstr "" -#: lib/mv_web/live/member_live/form.ex:51 -#: lib/mv_web/live/member_live/index.html.heex:207 -#: lib/mv_web/live/member_live/show.ex:56 +#: lib/mv_web/live/member_live/form.ex:50 +#: lib/mv_web/live/member_live/index.html.heex:215 +#: lib/mv_web/live/member_live/show.ex:55 #, elixir-autogen, elixir-format msgid "Join Date" msgstr "" @@ -88,7 +92,7 @@ msgstr "" msgid "New Member" 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 #, elixir-autogen, elixir-format msgid "Show" @@ -109,52 +113,52 @@ msgstr "" msgid "close" msgstr "" -#: lib/mv_web/live/member_live/form.ex:48 -#: lib/mv_web/live/member_live/show.ex:51 -#, elixir-autogen, elixir-format -msgid "Birth Date" -msgstr "" - -#: lib/mv_web/live/member_live/form.ex:52 -#: lib/mv_web/live/member_live/show.ex:57 +#: lib/mv_web/live/member_live/form.ex:51 +#: lib/mv_web/live/member_live/show.ex:56 #, elixir-autogen, elixir-format msgid "Exit Date" msgstr "" -#: lib/mv_web/live/member_live/form.ex:56 -#: lib/mv_web/live/member_live/index.html.heex:135 -#: lib/mv_web/live/member_live/show.ex:61 +#: lib/mv_web/live/member_live/form.ex:55 +#: lib/mv_web/live/member_live/index.html.heex:143 +#: lib/mv_web/live/member_live/show.ex:60 #, elixir-autogen, elixir-format msgid "House Number" msgstr "" -#: lib/mv_web/live/member_live/form.ex:53 -#: lib/mv_web/live/member_live/show.ex:58 +#: lib/mv_web/live/contribution_period_live/show.ex:140 +#: lib/mv_web/live/member_live/form.ex:52 +#: lib/mv_web/live/member_live/show.ex:57 #, elixir-autogen, elixir-format msgid "Notes" msgstr "" -#: lib/mv_web/live/member_live/form.ex:49 -#: lib/mv_web/live/member_live/show.ex:52 +#: lib/mv_web/live/components/payment_filter_component.ex:94 +#: 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 msgid "Paid" msgstr "" -#: lib/mv_web/live/member_live/form.ex:50 -#: lib/mv_web/live/member_live/index.html.heex:189 -#: lib/mv_web/live/member_live/show.ex:55 +#: lib/mv_web/live/member_live/form.ex:49 +#: lib/mv_web/live/member_live/index.html.heex:197 +#: lib/mv_web/live/member_live/show.ex:54 #, elixir-autogen, elixir-format msgid "Phone Number" msgstr "" -#: lib/mv_web/live/member_live/form.ex:57 -#: lib/mv_web/live/member_live/index.html.heex:153 -#: lib/mv_web/live/member_live/show.ex:62 +#: lib/mv_web/live/member_live/form.ex:56 +#: lib/mv_web/live/member_live/index.html.heex:161 +#: lib/mv_web/live/member_live/show.ex:61 #, elixir-autogen, elixir-format msgid "Postal Code" msgstr "" -#: lib/mv_web/live/member_live/form.ex:80 +#: lib/mv_web/live/member_live/form.ex:79 #, elixir-autogen, elixir-format msgid "Save Member" msgstr "" @@ -162,15 +166,15 @@ msgstr "" #: 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/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 #, elixir-autogen, elixir-format msgid "Saving..." msgstr "" -#: lib/mv_web/live/member_live/form.ex:55 -#: lib/mv_web/live/member_live/index.html.heex:117 -#: lib/mv_web/live/member_live/show.ex:60 +#: lib/mv_web/live/member_live/form.ex:54 +#: lib/mv_web/live/member_live/index.html.heex:125 +#: lib/mv_web/live/member_live/show.ex:59 #, elixir-autogen, elixir-format msgid "Street" msgstr "" @@ -180,13 +184,14 @@ msgstr "" msgid "Id" 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/show.ex:53 +#: lib/mv_web/live/member_live/show.ex:52 #, elixir-autogen, elixir-format msgid "No" msgstr "" -#: lib/mv_web/live/member_live/show.ex:116 +#: lib/mv_web/live/member_live/show.ex:115 #, elixir-autogen, elixir-format msgid "Show Member" msgstr "" @@ -196,22 +201,23 @@ msgstr "" msgid "This is a member record from your database." 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/show.ex:53 +#: lib/mv_web/live/member_live/show.ex:52 #, elixir-autogen, elixir-format msgid "Yes" msgstr "" #: 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/member_live/form.ex:138 +#: lib/mv_web/live/member_live/form.ex:137 #, elixir-autogen, elixir-format msgid "create" msgstr "" #: 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/member_live/form.ex:139 +#: lib/mv_web/live/member_live/form.ex:138 #, elixir-autogen, elixir-format msgid "update" msgstr "" @@ -221,7 +227,7 @@ msgstr "" msgid "Incorrect email or password" msgstr "" -#: lib/mv_web/live/member_live/form.ex:145 +#: lib/mv_web/live/member_live/form.ex:144 #, elixir-autogen, elixir-format msgid "Member %{action} successfully" msgstr "" @@ -254,7 +260,7 @@ msgstr "" #: 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_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 #, elixir-autogen, elixir-format msgid "Cancel" @@ -290,7 +296,7 @@ msgstr "" msgid "Immutable" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:102 +#: lib/mv_web/components/layouts/navbar.ex:113 #, elixir-autogen, elixir-format msgid "Logout" msgstr "" @@ -307,12 +313,15 @@ msgid "Member" msgstr "" #: 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.html.heex:3 #, elixir-autogen, elixir-format msgid "Members" msgstr "" +#: lib/mv_web/live/contribution_type_live/index.ex:48 #: lib/mv_web/live/custom_field_live/form.ex:51 #, elixir-autogen, elixir-format msgid "Name" @@ -333,6 +342,7 @@ msgstr "" msgid "Not set" 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:115 #: lib/mv_web/live/user_live/form.ex:224 @@ -351,7 +361,7 @@ msgstr "" msgid "Password Authentication" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:95 +#: lib/mv_web/components/layouts/navbar.ex:106 #, elixir-autogen, elixir-format msgid "Profil" msgstr "" @@ -361,17 +371,17 @@ msgstr "" msgid "Required" 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 msgid "Select all members" 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 msgid "Select member" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:99 +#: lib/mv_web/components/layouts/navbar.ex:110 #, elixir-autogen, elixir-format msgid "Settings" msgstr "" @@ -511,7 +521,7 @@ msgstr "" msgid "Linked Member" msgstr "" -#: lib/mv_web/live/member_live/show.ex:63 +#: lib/mv_web/live/member_live/show.ex:62 #, elixir-autogen, elixir-format msgid "Linked User" msgstr "" @@ -522,7 +532,7 @@ msgstr "" msgid "No member linked" msgstr "" -#: lib/mv_web/live/member_live/show.ex:73 +#: lib/mv_web/live/member_live/show.ex:72 #, elixir-autogen, elixir-format msgid "No user linked" msgstr "" @@ -539,20 +549,20 @@ msgstr "" msgid "Back to users list" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:33 -#: lib/mv_web/components/layouts/navbar.ex:39 +#: lib/mv_web/components/layouts/navbar.ex:44 +#: lib/mv_web/components/layouts/navbar.ex:50 #, elixir-autogen, elixir-format msgid "Select language" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:46 -#: lib/mv_web/components/layouts/navbar.ex:66 +#: lib/mv_web/components/layouts/navbar.ex:57 +#: lib/mv_web/components/layouts/navbar.ex:77 #, elixir-autogen, elixir-format msgid "Toggle dark mode" msgstr "" #: 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 msgid "Search..." msgstr "" @@ -568,7 +578,7 @@ msgstr "" msgid "Click to sort" 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 msgid "First name" msgstr "" @@ -609,8 +619,8 @@ msgstr "" msgid "Choose a custom field" msgstr "" -#: lib/mv_web/live/member_live/form.ex:59 -#: lib/mv_web/live/member_live/show.ex:78 +#: lib/mv_web/live/member_live/form.ex:58 +#: lib/mv_web/live/member_live/show.ex:77 #, elixir-autogen, elixir-format msgid "Custom Field Values" msgstr "" @@ -707,6 +717,7 @@ msgstr "" msgid "Association Name" 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:41 #, elixir-autogen, elixir-format @@ -718,6 +729,7 @@ msgstr "" msgid "Manage global settings for the association." msgstr "" +#: lib/mv_web/live/contribution_settings_live.ex:102 #: lib/mv_web/live/global_settings_live.ex:56 #, elixir-autogen, elixir-format msgid "Save Settings" @@ -778,7 +790,7 @@ msgstr "" msgid "Unlinking scheduled" msgstr "" -#: lib/mv_web/live/member_live/index.ex:164 +#: lib/mv_web/live/member_live/index.ex:165 #, elixir-autogen, elixir-format msgid "Copied %{count} email address to clipboard" msgid_plural "Copied %{count} email addresses to clipboard" @@ -795,12 +807,12 @@ msgstr "" msgid "Copy emails" msgstr "" -#: lib/mv_web/live/member_live/index.ex:153 +#: lib/mv_web/live/member_live/index.ex:154 #, elixir-autogen, elixir-format msgid "No email addresses found" msgstr "" -#: lib/mv_web/live/member_live/index.ex:150 +#: lib/mv_web/live/member_live/index.ex:151 #, elixir-autogen, elixir-format msgid "No members selected" msgstr "" @@ -815,7 +827,7 @@ msgstr "" msgid "Open in email program" msgstr "" -#: lib/mv_web/live/member_live/index.ex:173 +#: lib/mv_web/live/member_live/index.ex:174 #, elixir-autogen, elixir-format msgid "Tip: Paste email addresses into the BCC field for privacy compliance" msgstr "" @@ -832,3 +844,460 @@ msgstr "" #, elixir-autogen, elixir-format msgid "This field cannot be empty" 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 "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 125ce4e..46b37bf 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -12,11 +12,12 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: lib/mv_web/components/core_components.ex:386 +#: lib/mv_web/live/contribution_period_live/show.ex:141 #, elixir-autogen, elixir-format msgid "Actions" 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 #, elixir-autogen, elixir-format msgid "Are you sure?" @@ -28,20 +29,22 @@ msgstr "" msgid "Attempting to reconnect" msgstr "" -#: lib/mv_web/live/member_live/form.ex:54 -#: lib/mv_web/live/member_live/index.html.heex:171 -#: lib/mv_web/live/member_live/show.ex:59 +#: lib/mv_web/live/member_live/form.ex:53 +#: lib/mv_web/live/member_live/index.html.heex:179 +#: lib/mv_web/live/member_live/show.ex:58 #, elixir-autogen, elixir-format msgid "City" 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 #, elixir-autogen, elixir-format msgid "Delete" 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/index.html.heex:66 #, elixir-autogen, elixir-format @@ -49,13 +52,14 @@ msgid "Edit" msgstr "" #: 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 msgid "Edit Member" 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/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/user_live/form.ex:46 #: lib/mv_web/live/user_live/index.html.heex:44 @@ -70,9 +74,9 @@ msgstr "" msgid "First Name" msgstr "" -#: lib/mv_web/live/member_live/form.ex:51 -#: lib/mv_web/live/member_live/index.html.heex:207 -#: lib/mv_web/live/member_live/show.ex:56 +#: lib/mv_web/live/member_live/form.ex:50 +#: lib/mv_web/live/member_live/index.html.heex:215 +#: lib/mv_web/live/member_live/show.ex:55 #, elixir-autogen, elixir-format msgid "Join Date" msgstr "" @@ -88,7 +92,7 @@ msgstr "" msgid "New Member" 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 #, elixir-autogen, elixir-format msgid "Show" @@ -109,52 +113,52 @@ msgstr "" msgid "close" msgstr "" -#: lib/mv_web/live/member_live/form.ex:48 -#: lib/mv_web/live/member_live/show.ex:51 -#, elixir-autogen, elixir-format -msgid "Birth Date" -msgstr "" - -#: lib/mv_web/live/member_live/form.ex:52 -#: lib/mv_web/live/member_live/show.ex:57 +#: lib/mv_web/live/member_live/form.ex:51 +#: lib/mv_web/live/member_live/show.ex:56 #, elixir-autogen, elixir-format msgid "Exit Date" msgstr "" -#: lib/mv_web/live/member_live/form.ex:56 -#: lib/mv_web/live/member_live/index.html.heex:135 -#: lib/mv_web/live/member_live/show.ex:61 +#: lib/mv_web/live/member_live/form.ex:55 +#: lib/mv_web/live/member_live/index.html.heex:143 +#: lib/mv_web/live/member_live/show.ex:60 #, elixir-autogen, elixir-format msgid "House Number" msgstr "" -#: lib/mv_web/live/member_live/form.ex:53 -#: lib/mv_web/live/member_live/show.ex:58 +#: lib/mv_web/live/contribution_period_live/show.ex:140 +#: lib/mv_web/live/member_live/form.ex:52 +#: lib/mv_web/live/member_live/show.ex:57 #, elixir-autogen, elixir-format msgid "Notes" msgstr "" -#: lib/mv_web/live/member_live/form.ex:49 -#: lib/mv_web/live/member_live/show.ex:52 +#: lib/mv_web/live/components/payment_filter_component.ex:94 +#: 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 msgid "Paid" msgstr "" -#: lib/mv_web/live/member_live/form.ex:50 -#: lib/mv_web/live/member_live/index.html.heex:189 -#: lib/mv_web/live/member_live/show.ex:55 +#: lib/mv_web/live/member_live/form.ex:49 +#: lib/mv_web/live/member_live/index.html.heex:197 +#: lib/mv_web/live/member_live/show.ex:54 #, elixir-autogen, elixir-format msgid "Phone Number" msgstr "" -#: lib/mv_web/live/member_live/form.ex:57 -#: lib/mv_web/live/member_live/index.html.heex:153 -#: lib/mv_web/live/member_live/show.ex:62 +#: lib/mv_web/live/member_live/form.ex:56 +#: lib/mv_web/live/member_live/index.html.heex:161 +#: lib/mv_web/live/member_live/show.ex:61 #, elixir-autogen, elixir-format msgid "Postal Code" msgstr "" -#: lib/mv_web/live/member_live/form.ex:80 +#: lib/mv_web/live/member_live/form.ex:79 #, elixir-autogen, elixir-format, fuzzy msgid "Save Member" msgstr "" @@ -162,15 +166,15 @@ msgstr "" #: 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/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 #, elixir-autogen, elixir-format msgid "Saving..." msgstr "" -#: lib/mv_web/live/member_live/form.ex:55 -#: lib/mv_web/live/member_live/index.html.heex:117 -#: lib/mv_web/live/member_live/show.ex:60 +#: lib/mv_web/live/member_live/form.ex:54 +#: lib/mv_web/live/member_live/index.html.heex:125 +#: lib/mv_web/live/member_live/show.ex:59 #, elixir-autogen, elixir-format msgid "Street" msgstr "" @@ -180,13 +184,14 @@ msgstr "" msgid "Id" 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/show.ex:53 +#: lib/mv_web/live/member_live/show.ex:52 #, elixir-autogen, elixir-format msgid "No" msgstr "" -#: lib/mv_web/live/member_live/show.ex:116 +#: lib/mv_web/live/member_live/show.ex:115 #, elixir-autogen, elixir-format, fuzzy msgid "Show Member" msgstr "" @@ -196,22 +201,23 @@ msgstr "" msgid "This is a member record from your database." 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/show.ex:53 +#: lib/mv_web/live/member_live/show.ex:52 #, elixir-autogen, elixir-format msgid "Yes" msgstr "" #: 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/member_live/form.ex:138 +#: lib/mv_web/live/member_live/form.ex:137 #, elixir-autogen, elixir-format msgid "create" msgstr "" #: 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/member_live/form.ex:139 +#: lib/mv_web/live/member_live/form.ex:138 #, elixir-autogen, elixir-format msgid "update" msgstr "" @@ -221,7 +227,7 @@ msgstr "" msgid "Incorrect email or password" msgstr "" -#: lib/mv_web/live/member_live/form.ex:145 +#: lib/mv_web/live/member_live/form.ex:144 #, elixir-autogen, elixir-format msgid "Member %{action} successfully" msgstr "" @@ -254,7 +260,7 @@ msgstr "" #: 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_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 #, elixir-autogen, elixir-format msgid "Cancel" @@ -290,7 +296,7 @@ msgstr "" msgid "Immutable" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:102 +#: lib/mv_web/components/layouts/navbar.ex:113 #, elixir-autogen, elixir-format msgid "Logout" msgstr "" @@ -307,12 +313,15 @@ msgid "Member" msgstr "" #: 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.html.heex:3 #, elixir-autogen, elixir-format msgid "Members" msgstr "" +#: lib/mv_web/live/contribution_type_live/index.ex:48 #: lib/mv_web/live/custom_field_live/form.ex:51 #, elixir-autogen, elixir-format msgid "Name" @@ -333,6 +342,7 @@ msgstr "" msgid "Not set" 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:115 #: lib/mv_web/live/user_live/form.ex:224 @@ -351,7 +361,7 @@ msgstr "" msgid "Password Authentication" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:95 +#: lib/mv_web/components/layouts/navbar.ex:106 #, elixir-autogen, elixir-format msgid "Profil" msgstr "" @@ -361,17 +371,17 @@ msgstr "" msgid "Required" 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 msgid "Select all members" 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 msgid "Select member" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:99 +#: lib/mv_web/components/layouts/navbar.ex:110 #, elixir-autogen, elixir-format msgid "Settings" msgstr "" @@ -511,7 +521,7 @@ msgstr "User will be created without a password. Check 'Set Password' to add one msgid "Linked Member" msgstr "" -#: lib/mv_web/live/member_live/show.ex:63 +#: lib/mv_web/live/member_live/show.ex:62 #, elixir-autogen, elixir-format msgid "Linked User" msgstr "" @@ -522,7 +532,7 @@ msgstr "" msgid "No member linked" msgstr "" -#: lib/mv_web/live/member_live/show.ex:73 +#: lib/mv_web/live/member_live/show.ex:72 #, elixir-autogen, elixir-format msgid "No user linked" msgstr "" @@ -539,20 +549,20 @@ msgstr "" msgid "Back to users list" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:33 -#: lib/mv_web/components/layouts/navbar.ex:39 +#: lib/mv_web/components/layouts/navbar.ex:44 +#: lib/mv_web/components/layouts/navbar.ex:50 #, elixir-autogen, elixir-format, fuzzy msgid "Select language" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:46 -#: lib/mv_web/components/layouts/navbar.ex:66 +#: lib/mv_web/components/layouts/navbar.ex:57 +#: lib/mv_web/components/layouts/navbar.ex:77 #, elixir-autogen, elixir-format msgid "Toggle dark mode" msgstr "" #: 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 msgid "Search..." msgstr "" @@ -568,7 +578,7 @@ msgstr "" msgid "Click to sort" 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 msgid "First name" msgstr "" @@ -609,8 +619,8 @@ msgstr "" msgid "Choose a custom field" msgstr "" -#: lib/mv_web/live/member_live/form.ex:59 -#: lib/mv_web/live/member_live/show.ex:78 +#: lib/mv_web/live/member_live/form.ex:58 +#: lib/mv_web/live/member_live/show.ex:77 #, elixir-autogen, elixir-format msgid "Custom Field Values" msgstr "" @@ -707,6 +717,7 @@ msgstr "" msgid "Association Name" 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:41 #, elixir-autogen, elixir-format, fuzzy @@ -718,6 +729,7 @@ msgstr "" msgid "Manage global settings for the association." msgstr "" +#: lib/mv_web/live/contribution_settings_live.ex:102 #: lib/mv_web/live/global_settings_live.ex:56 #, elixir-autogen, elixir-format, fuzzy msgid "Save Settings" @@ -778,7 +790,7 @@ msgstr "" msgid "Unlinking scheduled" msgstr "" -#: lib/mv_web/live/member_live/index.ex:164 +#: lib/mv_web/live/member_live/index.ex:165 #, elixir-autogen, elixir-format msgid "Copied %{count} email address to clipboard" msgid_plural "Copied %{count} email addresses to clipboard" @@ -795,12 +807,12 @@ msgstr "" msgid "Copy emails" msgstr "" -#: lib/mv_web/live/member_live/index.ex:153 +#: lib/mv_web/live/member_live/index.ex:154 #, elixir-autogen, elixir-format msgid "No email addresses found" msgstr "" -#: lib/mv_web/live/member_live/index.ex:150 +#: lib/mv_web/live/member_live/index.ex:151 #, elixir-autogen, elixir-format, fuzzy msgid "No members selected" msgstr "" @@ -815,7 +827,7 @@ msgstr "" msgid "Open in email program" msgstr "" -#: lib/mv_web/live/member_live/index.ex:173 +#: lib/mv_web/live/member_live/index.ex:174 #, elixir-autogen, elixir-format msgid "Tip: Paste email addresses into the BCC field for privacy compliance" msgstr "" @@ -832,3 +844,460 @@ msgstr "" #, elixir-autogen, elixir-format, fuzzy msgid "This field cannot be empty" 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 "" diff --git a/priv/repo/migrations/20251202145404_remove_birth_date_from_members.exs b/priv/repo/migrations/20251202145404_remove_birth_date_from_members.exs new file mode 100644 index 0000000..4a6cf3a --- /dev/null +++ b/priv/repo/migrations/20251202145404_remove_birth_date_from_members.exs @@ -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 diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 542e559..bec9006 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -112,7 +112,6 @@ for member_attrs <- [ first_name: "Hans", last_name: "Müller", email: "hans.mueller@example.de", - birth_date: ~D[1985-06-15], join_date: ~D[2023-01-15], paid: true, phone_number: "+49301234567", @@ -125,7 +124,6 @@ for member_attrs <- [ first_name: "Greta", last_name: "Schmidt", email: "greta.schmidt@example.de", - birth_date: ~D[1990-03-22], join_date: ~D[2023-02-01], paid: false, phone_number: "+49309876543", @@ -139,7 +137,6 @@ for member_attrs <- [ first_name: "Friedrich", last_name: "Wagner", email: "friedrich.wagner@example.de", - birth_date: ~D[1978-11-08], join_date: ~D[2022-11-10], paid: true, phone_number: "+49301122334", @@ -151,7 +148,6 @@ for member_attrs <- [ first_name: "Marianne", last_name: "Wagner", email: "marianne.wagner@example.de", - birth_date: ~D[1978-11-08], join_date: ~D[2022-11-10], paid: true, phone_number: "+49301122334", @@ -186,7 +182,6 @@ linked_members = [ first_name: "Maria", last_name: "Weber", email: "maria.weber@example.de", - birth_date: ~D[1992-07-14], join_date: ~D[2023-03-15], paid: true, phone_number: "+49301357924", @@ -202,7 +197,6 @@ linked_members = [ first_name: "Thomas", last_name: "Klein", email: "thomas.klein@example.de", - birth_date: ~D[1988-12-03], join_date: ~D[2023-04-01], paid: false, phone_number: "+49302468135", diff --git a/test/membership/member_test.exs b/test/membership/member_test.exs index 7015d34..1bf594a 100644 --- a/test/membership/member_test.exs +++ b/test/membership/member_test.exs @@ -6,7 +6,6 @@ defmodule Mv.Membership.MemberTest do @valid_attrs %{ first_name: "John", last_name: "Doe", - birth_date: ~D[1990-01-01], paid: true, email: "john@example.com", phone_number: "+49123456789", @@ -43,12 +42,6 @@ defmodule Mv.Membership.MemberTest do assert error_message(errors, :email) =~ "is not a valid email" 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 attrs = Map.put(@valid_attrs, :paid, nil) attrs2 = Map.put(@valid_attrs, :paid, "yes") diff --git a/test/mv_web/components/payment_filter_component_test.exs b/test/mv_web/components/payment_filter_component_test.exs new file mode 100644 index 0000000..c44bf41 --- /dev/null +++ b/test/mv_web/components/payment_filter_component_test.exs @@ -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 diff --git a/test/mv_web/member_live/index_custom_fields_display_test.exs b/test/mv_web/member_live/index_custom_fields_display_test.exs index 25aefe5..0485f5e 100644 --- a/test/mv_web/member_live/index_custom_fields_display_test.exs +++ b/test/mv_web/member_live/index_custom_fields_display_test.exs @@ -9,7 +9,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do - Custom field values are correctly formatted for different types - 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 require Ash.Query diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs index e3ad5bb..0bcc731 100644 --- a/test/mv_web/member_live/index_test.exs +++ b/test/mv_web/member_live/index_test.exs @@ -469,4 +469,221 @@ defmodule MvWeb.MemberLive.IndexTest do assert has_element?(view, "#flash-group") 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