# Mitgliederverwaltung für Pfadfindervereine — Requirements & Architecture

**Version:** 2.1  
**Status:** Draft  
**Last updated:** 2026-04-07

## 1. Project Overview

A custom Nextcloud app (NC 28+) for managing scout group (Pfadfinderverein) members. Serves as the **single source of truth** for all member data, deeply integrated into the Nextcloud ecosystem (Calendar, Contacts, Files). German-language UI only. Designed for potential future open-sourcing but not prioritized.

**Target scale:** 50–200 members, 3–10 admin users  
**Database:** MySQL/MariaDB (shared with Nextcloud instance)  
**Tech stack:** PHP backend (Nextcloud App Framework), Vue.js 3 frontend  
**App ID:** `mitgliederverwaltung`  
**Security posture:** High — coded to latest security standards, thoroughly tested and pentested. Banking data encrypted at rest.

---

## 2. Functional Requirements

### 2.1 Member Management (Mitgliederverwaltung)

Each member record contains:

| Field | Type | Notes |
|---|---|---|
| Vorname, Nachname | text | required |
| Geburtsdatum | date | required; age auto-calculated |
| Geschlecht | enum | optional |
| Rolle | enum | `Mitglied` or `Erziehungsberechtigter` |
| Stufe | enum | Wölflinge, Pfadfinder, Rover (configurable list) |
| Eintritt | date | required; for Mitgliedsdauer calculation |
| Austritt | date | nullable; if set → inactive member |
| Status | enum | `aktiv`, `inaktiv`, `gelöscht` (soft-deleted) |
| Allergien | text | freeform, visible on reports |
| Notizen | richtext | general notes |
| Zusätzliche Notizen | richtext | secondary notes (e.g. medical, behavioral) |
| E-Mail | text | optional, multiple allowed |
| Telefon | text | E.164 format enforced (+49...), multiple allowed |
| Adressen | structured[] | multiple addresses allowed (e.g. separated parents). Fields: Label, Straße, PLZ, Ort, Land |
| Bankverbindung | relation | → Family banking (shared) or individual override |
| Krankenversicherung | structured | Typ: `privat` or `gesetzlich` (enum) + Name der Versicherung (text). Only relevant for active Mitglieder, not Erziehungsberechtigte. |
| Mitgliederordner | file link | link to folder in Nextcloud Files (configurable path pattern) |
| Mitgliederantrag | file link | specific file within member folder |

**Derived/computed fields:**
- **Alter** — calculated from Geburtsdatum, displayed everywhere, updated daily
- **Mitgliedsdauer** — years and months since Eintritt. Used to trigger Jubiläum milestones (see 2.8)
- **Beitragsstatus** — current year paid/unpaid

**Erziehungsberechtigter (non-paying parent) role:**
- Linked to family, contact details stored (phone, email, address)
- Does NOT get a birthday calendar event
- Does NOT appear in fee calculations
- DOES appear in contact lists and family views
- DOES get a Nextcloud Contact entry

### 2.2 Family Management (Familienverwaltung)

Families group members who share banking details and optionally addresses.

| Field | Type | Notes |
|---|---|---|
| Familienname | text | e.g. "Familie Müller" |
| Bankverbindung | structured | Kontoinhaber, IBAN (validated + encrypted), BIC, Kreditinstitut |
| Ansprechpartner | relation | → one or more Erziehungsberechtigter members |

**Family ↔ Member link:**
- A member belongs to 0 or 1 family
- Role within family: `Kind` or `Erziehungsberechtigter`
- Members have their own address(es) — no shared family address (since separated parents need multiple addresses)
- Members can override the family banking with their own

**Active children count** — only active members with role `Kind` in the family. Drives fee discount calculation.

### 2.3 Stufen Management

- Configurable list of Stufen (default: **Wölflinge, Pfadfinder, Rover**)
- Each Stufe has: Name, typical age range (display only), optional color
- Members are assigned to exactly one Stufe at a time
- Stufe history tracked (when a member moved between Stufen, with date)
- **Suggested Stufe changes:** The app shows a notification/list when members reach the typical age boundary for the next Stufe. Stufenwechsel is always manual — the suggestion is advisory only.

### 2.4 Fee System (Beitragsverwaltung)

**Billing period:** Calendar year (January–December)  
**Collection:** Manually, typically in January  
**No partial payments:** Fees are always paid in full  
**No proration:** Members who join mid-year pay the full amount for that year  
**No refund on exit:** Members who leave mid-year do not get refunds

**Fee model — key complexity:**
- Fee amounts can change year to year (Vorstand decides a new rate)
- **Active members** pay the current rate
- **Inactive members** (who still pay) continue paying the rate that was current when they became inactive — they are not affected by fee increases
- **Erziehungsberechtigte** (non-paying parents) are excluded entirely

**Family discount — configurable DSL:**

The fee rules must be configurable to express logic like:

```
1 Kind in Familie → 60€
2 Kinder in Familie → 120€ (= 2 × 60€)
3+ Kinder in Familie → 120€ (3., 4., ... Kind kostenlos)
```

**DSL approach:** A small rule table in the settings, structured as:

| Anzahl aktive Kinder | Gesamtbeitrag | Notiz |
|---|---|---|
| 1 | 60.00 | Einzelkind |
| 2 | 120.00 | 2 Kinder |
| 3+ | 120.00 | Ab 3. Kind kostenlos |

This is stored as a JSON array in the app config. The fee engine reads this table and looks up the total family fee based on the count of active children, then divides appropriately (or assigns per-child amounts as configured).

**Alternative: per-member rule approach** for more flexibility:

```json
{
  "base_rate": 60.00,
  "family_rules": [
    { "child_number": 1, "rate": 60.00 },
    { "child_number": 2, "rate": 60.00 },
    { "child_number": "3+", "rate": 0.00 }
  ],
  "inactive_rule": "freeze_rate_at_exit"
}
```

The engine processes:
1. Get all active Kinder in the family, sorted by age (oldest first)
2. Assign each child the rate for their position (1st, 2nd, 3rd+)
3. For inactive members: look up their `frozen_rate` (stored when they became inactive)

**Fee records:**
- One record per member per year
- Fields: year, calculated amount, rule applied (snapshot), paid (yes/no), payment date, notes
- Flag: `manuell_angepasst` — if an admin overrides the calculated fee, it is never auto-overwritten
- Batch action: "Beiträge für Jahr X berechnen" generates/updates all records (skips manually adjusted ones)
- When a member becomes inactive: store `frozen_rate` on their record (the rate they'll continue paying)

### 2.5 Search, Filtering & Saved Queries

**Global search bar:** Full-text search across Vorname, Nachname, Familienname, Adressen, Notizen, Allergien

**Filter/query system — granular and saveable:**

Admins can build complex queries using a visual query builder:

- **Fields:** any member/family field (Alter, Stufe, Status, Eintritt, Austritt, Beitragsstatus, Mitgliedsdauer, Rolle, Adresse.Ort, etc.)
- **Operators:** `=`, `≠`, `<`, `>`, `<=`, `>=`, `enthält`, `beginnt mit`, `ist leer`, `ist nicht leer`
- **Logic:** AND / OR grouping with nesting
- **Examples:**
  - "Alle aktiven Mitglieder unter 14 Jahren" → `Status = aktiv AND Alter < 14`
  - "Unbezahlte Beiträge 2026" → `Beitragsstatus(2026) = unbezahlt`
  - "Rover ohne E-Mail" → `Stufe = Rover AND E-Mail ist leer`
  - "Mitglieder aus PLZ 8xxxx" → `Adresse.PLZ beginnt mit 8`

**Saved queries:**
- Each query can be saved with a user-defined name (e.g. "Minderjährige aktive Mitglieder")
- Saved queries appear as quick-access buttons/list in the sidebar
- Saved queries are shared across all admins (with appropriate permission level)
- Sort order is part of the saved query

**Preset quick filters:**
- "Aktive Mitglieder"
- "Inaktive Mitglieder"
- "Unbezahlte Beiträge"
- "Geburtstage diesen Monat"

### 2.6 Reports & Exports

All reports available as **PDF and CSV**.

| Report | Content |
|---|---|
| Mitgliederliste | all or filtered members with key fields |
| Beitragsliste | per year: who owes what, who has paid, totals |
| Stufenliste | members grouped by Stufe |
| Allergieliste | all members with allergies and Krankenversicherung (for camps/events) |
| Geburtstagsliste | upcoming birthdays |
| Kontaktliste | addresses + phone numbers, optionally per Stufe |
| Bankverbindungen | for manual SEPA processing (restricted access) |
| Familienliste | families with their members |
| Lagerhistorie | which camps a member attended (see 2.9) |
| Verletzungsprotokoll | injury records for a given period or member (see 2.10) |

**Any saved query can be exported** as PDF or CSV directly.

**PDF generation:** Use a PHP PDF library (TCPDF or FPDF) server-side. Reports have a configurable header with Verein name/logo.

**CSV export:** Standard CSV with UTF-8 BOM for Excel compatibility.

**Encrypted export:** When exporting sensitive data (banking, full member details), the export file must be encrypted. Options: password-protected ZIP or GPG-encrypted file. The user is prompted for a password before download.

### 2.7 Data Import

- CSV/Excel import wizard for initial migration
- Column mapping UI: user maps source columns to member fields
- Preview before import with validation errors highlighted
- Support creating families from import data (e.g. grouping by shared Nachname + Adresse)
- Dry-run mode: show what would be created without committing
- Duplicate detection on import (name + birthday match)

### 2.8 Membership Milestones (Jubiläen)

- **Mitgliedsdauer** is calculated from Eintritt and displayed on member records
- The system flags upcoming milestones: **25, 50, 60, 70 Jahre** Mitgliedschaft (configurable list)
- A dedicated view/report: "Anstehende Jubiläen" shows members reaching a milestone in the current or upcoming year
- No automatic notifications for now — just a visible list/report

### 2.9 Lagertracking (Camp Management)

Each Lager (camp/event) is a standalone entity that can span multiple Stufen.

| Field | Type | Notes |
|---|---|---|
| Name | text | e.g. "Sommerlager Trupp 2026" |
| Startdatum | date | required |
| Enddatum | date | required |
| Ort | text | location/address |
| Stufen | enum[] | which Stufen participated (multi-select) |
| Teilnehmer | relation[] | → members who attended |
| Beschreibung | richtext | optional notes |
| Dateien | file links[] | links to files in Nextcloud (Kostenabrechnung, Einladung, Zeitplan, etc.) |

**Features:**
- CRUD for Lager with a form UI
- Add/remove members as Teilnehmer (with search/autocomplete)
- Link files from Nextcloud Files (from a configurable Lager base folder, or arbitrary paths)
- **Per-member camp history:** on a member's detail page, show all Lager they attended
- **Export:** "Lagerhistorie für [Mitglied]" — list of all camps attended with dates and roles
- **Lager overview:** list of all past and upcoming camps, filterable by year, Stufe

### 2.10 Injury Tracking (Verletzungsprotokoll)

Simple text-based injury log linked to members and optionally to a Lager.

| Field | Type | Notes |
|---|---|---|
| Datum | date | when the injury occurred |
| Betroffenes Mitglied | relation | → member who was injured |
| Beteiligte | relation[] | → other members involved (optional) |
| Lager/Aktivität | relation | → Lager (optional) or freetext activity name |
| Beschreibung | richtext | what happened, what was done |
| Erstellt von | auto | Nextcloud user who logged the entry |

**Features:**
- Log entries from member detail page or from Lager detail page
- View all injuries for a member, or all injuries for a Lager
- Exportable as report (PDF/CSV) for a date range or per member
- Audit-logged like all other data changes

---

## 3. Nextcloud Integrations

### 3.1 Calendar Integration (CalDAV)

- App creates/manages a dedicated calendar (e.g. "Pfadfinder Geburtstage")
- For each active member with role `Mitglied` (NOT Erziehungsberechtigter):
  - A recurring yearly all-day event on their birthday
  - Event title format: **"🎂 Vorname Nachname (*Geburtsjahr)"** — e.g. "🎂 Max Müller (*2012)"
  - The birth year is static in the title; the calendar app itself shows which anniversary it is
  - No current age in the title — it would require annual updates
- **Sync behavior:**
  - Member created (role=Mitglied) → calendar event created
  - Birthday changed → event updated
  - Member deactivated (Austritt set) → event deleted
  - Member reactivated → event re-created
  - Role changed to/from Erziehungsberechtigter → event created/deleted accordingly
- Use Nextcloud's `\OCA\DAV\CalDAV\CalDavBackend` or the internal CalDAV API

### 3.2 Contacts Integration (CardDAV)

- App creates/manages a dedicated address book (e.g. "Pfadfinder Mitglieder")
- For each active member (including Erziehungsberechtigte): a VCard with name, address(es), phone, email, birthday
- **Sync behavior:** same pattern as calendar — create/update/delete on member changes
- Use Nextcloud's `\OCA\DAV\CardDAV\CardDavBackend`

### 3.3 Files Integration

- Each member is linked to a **folder** in Nextcloud Files
- **Configurable base path** in app settings (e.g. `/Verein/Mitglieder/`)
- **Configurable subfolder pattern** (e.g. `{Nachname}_{Vorname}/` or `{Nachname}, {Vorname}/`)
- The member folder can contain arbitrary files: Mitgliederantrag, medical docs, correspondence, etc.
- The app shows the folder contents inline on the member detail page and allows navigating to Nextcloud Files
- Optional: "Datei hochladen" button that uploads directly into the member's folder
- Optional: auto-create member subfolder on member creation (configurable toggle)
- **Lager files** follow a similar pattern: configurable Lager base path + subfolder per Lager
- Uses Nextcloud's `\OCP\Files\IRootFolder` API

---

## 4. Non-Functional Requirements

### 4.1 Access Control

- Uses Nextcloud's built-in user/group system for authentication
- App-level permission model, **configurable per Nextcloud user**:

| Permission level | Can do |
|---|---|
| Kein Zugriff | App not visible |
| Lesezugriff | View members, search, generate reports (no banking details) |
| Stufenzugriff | Read/write for members in assigned Stufe(n) only |
| Vollzugriff | Full CRUD on all data |
| Admin | Vollzugriff + app settings, fee rules, user permissions, import |

- Banking details (IBAN etc.) are a **separate visibility flag** — can be hidden even from Vollzugriff users
- Injury records: visible to all users with Lesezugriff or above
- Permission management UI in app settings (Admin only)

### 4.2 Audit Log

Full audit trail stored in a dedicated DB table:

| Field | Content |
|---|---|
| Zeitpunkt | timestamp |
| Benutzer | Nextcloud user who made the change |
| Aktion | create / update / delete / soft-delete |
| Entität | member / family / fee_record / lager / injury / setting / permission |
| Entität-ID | ID of affected record |
| Feld | which field changed (for updates) |
| Alter Wert | previous value (encrypted fields show "[verschlüsselt]") |
| Neuer Wert | new value (encrypted fields show "[verschlüsselt]") |

- Viewable by Admin users in the app
- Filterable by entity, user, date range
- Audit log entries are immutable — no editing or deletion
- Retention: keep indefinitely (configurable max age as future option)

### 4.3 Security Requirements

**This is a high-priority area. The app handles sensitive personal data (minors, banking, health/allergy info).**

**Data at rest:**
- IBAN and Kontoinhaber encrypted using `\OCP\Security\ICrypto`
- Consider encrypting Allergien and medical notes as well
- Soft-deleted member data: banking, allergies, and sensitive notes are **hard-deleted** on soft-delete. Only name, Eintritt, Austritt, and Mitgliedsdauer are retained.

**Data in transit:**
- All API endpoints require Nextcloud authentication (no anonymous access)
- CSRF protection on all state-changing endpoints
- API input validation and sanitization on every endpoint

**Application security:**
- Parameterized queries only (via QBMapper) — no raw SQL
- Output escaping in Vue templates (Vue's default behavior)
- Rate limiting on search/export endpoints to prevent data scraping
- No sensitive data in URLs or browser logs
- Session timeout follows Nextcloud's settings

**Export security:**
- Exports containing banking or sensitive data must be encrypted (password-protected ZIP or GPG)
- Export actions are audit-logged

**Testing requirements:**
- Unit tests for fee calculation engine, permission checks, validation logic
- Integration tests for API endpoints (auth, CRUD, edge cases)
- Security-specific tests: injection attempts, permission bypass, CSRF
- Manual pentest before production deployment (or use automated tools like OWASP ZAP)

**Dependency management:**
- Pin all PHP and JS dependencies to exact versions
- Regular dependency audit (Composer audit, npm audit)

### 4.4 Data Validation

- IBAN validation (checksum + country format)
- Phone numbers: E.164 format enforced (+49...), validated on input with helpful formatting hints
- Date plausibility (birthday not in future, not >120 years ago; Eintritt not before birthday)
- Required field enforcement on save
- Duplicate detection: warn when creating a member with matching Vorname + Nachname + Geburtsdatum

### 4.5 Soft Deletion & Data Retention

When a member is soft-deleted:
- **Retained:** Vorname, Nachname, Geburtsdatum, Eintritt, Austritt, Mitgliedsdauer, Stufe history, Lager attendance, fee history
- **Hard-deleted:** Banking details (IBAN, Kontoinhaber, BIC), Allergien, Krankenversicherung, Notizen, Zusätzliche Notizen, Adressen, Telefon, E-Mail
- **Calendar:** birthday event removed
- **Contacts:** VCard removed
- **Files:** member folder is NOT deleted (manual cleanup)
- Soft-deleted members are hidden from normal views but visible in an "Archiv" view (Admin only)
- A "DSGVO-Löschung" action performs full hard-delete including retained fields (irreversible, audit-logged)

### 4.6 DSGVO / Data Privacy

- **Auskunftsrecht:** Export all stored data for a single member as encrypted PDF/JSON
- **Recht auf Löschung:** Full hard-delete action (see 4.5)
- **Einwilligung:** Optional date field per member for date of data processing consent
- **Datenschutzhinweis:** Configurable text shown during member creation (informational)

---

## 5. Architecture Hints

### 5.1 Nextcloud App Structure

```
mitgliederverwaltung/
├── appinfo/
│   ├── info.xml              ← app metadata, NC 28+ constraint
│   └── routes.php            ← API route definitions
├── lib/
│   ├── AppInfo/
│   │   └── Application.php   ← app bootstrap, DI container
│   ├── Controller/
│   │   ├── MemberController.php
│   │   ├── FamilyController.php
│   │   ├── FeeController.php
│   │   ├── LagerController.php
│   │   ├── InjuryController.php
│   │   ├── ReportController.php
│   │   ├── ImportController.php
│   │   ├── QueryController.php      ← saved queries CRUD
│   │   └── SettingsController.php
│   ├── Db/
│   │   ├── Member.php / MemberMapper.php
│   │   ├── Family.php / FamilyMapper.php
│   │   ├── FeeRecord.php / FeeRecordMapper.php
│   │   ├── FeeRule.php / FeeRuleMapper.php
│   │   ├── Lager.php / LagerMapper.php
│   │   ├── LagerTeilnehmer.php / LagerTeilnehmerMapper.php
│   │   ├── Injury.php / InjuryMapper.php
│   │   ├── SavedQuery.php / SavedQueryMapper.php
│   │   ├── StufeHistory.php / StufeHistoryMapper.php
│   │   ├── AuditLog.php / AuditLogMapper.php
│   │   └── Permission.php / PermissionMapper.php
│   ├── Service/
│   │   ├── MemberService.php
│   │   ├── FamilyService.php
│   │   ├── FeeCalculationService.php  ← fee rules engine
│   │   ├── CalendarSyncService.php
│   │   ├── ContactsSyncService.php
│   │   ├── FileLinkService.php
│   │   ├── LagerService.php
│   │   ├── InjuryService.php
│   │   ├── ReportService.php
│   │   ├── ImportService.php
│   │   ├── QueryService.php           ← query builder → SQL
│   │   ├── AuditService.php
│   │   ├── PermissionService.php
│   │   ├── EncryptionService.php      ← wraps ICrypto for field-level encryption
│   │   └── MilestoneService.php       ← Jubiläen detection
│   └── Migration/
│       └── Version000001Date*.php
├── src/                       ← Vue.js 3 frontend
│   ├── main.js
│   ├── App.vue
│   ├── router.js
│   ├── stores/                ← Pinia
│   │   ├── members.js
│   │   ├── families.js
│   │   ├── fees.js
│   │   ├── lager.js
│   │   └── queries.js
│   ├── views/
│   │   ├── MemberList.vue
│   │   ├── MemberDetail.vue   ← tabbed: Persönlich, Familie, Beitrag, Lager, Verletzungen, Dateien, Verlauf
│   │   ├── FamilyList.vue
│   │   ├── FamilyDetail.vue
│   │   ├── FeeOverview.vue
│   │   ├── LagerList.vue
│   │   ├── LagerDetail.vue
│   │   ├── Reports.vue
│   │   ├── Import.vue
│   │   ├── QueryBuilder.vue
│   │   ├── Milestones.vue
│   │   ├── AuditLog.vue
│   │   └── Settings.vue
│   └── components/
│       ├── MemberForm.vue
│       ├── FamilyForm.vue
│       ├── SearchBar.vue
│       ├── QueryBuilder.vue   ← visual AND/OR filter builder
│       ├── LagerForm.vue
│       ├── InjuryForm.vue
│       ├── PhoneInput.vue     ← E.164 formatter
│       └── FileExplorer.vue   ← inline NC file browser
├── templates/
│   └── index.php
├── tests/
│   ├── Unit/
│   │   ├── FeeCalculationServiceTest.php
│   │   ├── PermissionServiceTest.php
│   │   └── ...
│   └── Integration/
│       ├── MemberControllerTest.php
│       └── ...
├── css/
└── img/
```

### 5.2 Database Schema (Key Tables)

```sql
-- Members
oc_mv_members (
  id, vorname, nachname, geburtsdatum, geschlecht,
  rolle ENUM('mitglied', 'erziehungsberechtigter'),
  stufe_id, eintritt, austritt,
  status ENUM('aktiv', 'inaktiv', 'geloescht'),
  allergien_encrypted, notizen, zusatz_notizen,
  kv_typ ENUM('privat', 'gesetzlich') NULL, kv_name VARCHAR(255) NULL,
  family_id FK, frozen_fee_rate,
  calendar_event_uri, contact_vcard_uri,
  created_at, updated_at, deleted_at
)

-- Addresses (multiple per member)
oc_mv_addresses (
  id, member_id FK, label, strasse, plz, ort, land, is_primary
)

-- Phone numbers (multiple per member, E.164)
oc_mv_phones (
  id, member_id FK, label, number_e164
)

-- Emails (multiple per member)
oc_mv_emails (
  id, member_id FK, label, email
)

-- Families
oc_mv_families (
  id, name, kontoinhaber_encrypted, iban_encrypted, bic, kreditinstitut,
  created_at, updated_at
)

-- Fee rules (JSON config, versioned)
oc_mv_fee_rules (
  id, year_from, base_rate, family_rules_json, inactive_rule,
  created_at, created_by
)

-- Fee records
oc_mv_fee_records (
  id, member_id FK, year, amount, rule_snapshot_json,
  manuell_angepasst, paid, payment_date, notes,
  created_at, updated_at
)

-- Stufen
oc_mv_stufen (
  id, name, sort_order, age_range_min, age_range_max, color
)

-- Stufe history
oc_mv_stufe_history (
  id, member_id FK, stufe_id FK, changed_at, changed_by
)

-- Lager
oc_mv_lager (
  id, name, startdatum, enddatum, ort, beschreibung,
  files_folder_path, created_at, updated_at
)

-- Lager ↔ Stufen (m:n)
oc_mv_lager_stufen (
  lager_id FK, stufe_id FK
)

-- Lager ↔ Members (m:n)
oc_mv_lager_teilnehmer (
  lager_id FK, member_id FK, rolle TEXT -- e.g. 'Teilnehmer', 'Leitung'
)

-- Lager files
oc_mv_lager_files (
  id, lager_id FK, file_path, label -- e.g. 'Kostenabrechnung', 'Zeitplan'
)

-- Injuries
oc_mv_injuries (
  id, datum, member_id FK, lager_id FK NULL,
  activity_name, beschreibung, created_by, created_at
)

-- Injury involved members (m:n)
oc_mv_injury_involved (
  injury_id FK, member_id FK
)

-- Saved queries
oc_mv_saved_queries (
  id, name, query_json, sort_json, created_by, created_at
)

-- Permissions
oc_mv_permissions (
  id, nc_user_id, level ENUM('none','read','stufe','full','admin'),
  allowed_stufen_json, can_see_banking BOOLEAN
)

-- Audit log (append-only)
oc_mv_audit_log (
  id, zeitpunkt, nc_user_id, aktion, entitaet, entitaet_id,
  feld, alter_wert, neuer_wert
)
```

### 5.3 Key Technical Decisions

**Calendar/Contacts sync strategy:**
- Don't sync on every save — queue sync jobs instead
- A `TimedJob` (every 5 min) processes the queue
- A nightly full-sync catches drift
- Store CalDAV event URI and CardDAV vCard URI on member records for efficient updates

**Fee calculation engine:**
- Fee rules stored in `oc_mv_fee_rules` with a `year_from` field (rules apply from that year until a newer rule exists)
- Calculation is a pure function: `(member, family, rules_for_year) → fee_amount`
- Inactive members: use their `frozen_fee_rate` instead of current rules
- Batch action: iterates all active+paying members, skips `manuell_angepasst` records
- Rule snapshots: each fee_record stores the rule that was applied as JSON (immutable audit trail)

**Saved query engine:**
- Queries stored as JSON AST: `{ "and": [{ "field": "alter", "op": "<", "value": 14 }, { "field": "status", "op": "=", "value": "aktiv" }] }`
- `QueryService` translates AST → Doctrine DBAL QueryBuilder conditions
- Validate field names against a whitelist to prevent injection
- Support nested AND/OR groups

**File linking:**
- Store file/folder paths (relative to base path), not file IDs
- Validate existence when displaying; show warning if missing
- Auto-create member subfolder on member creation (configurable)

**Encryption:**
- Dedicated `EncryptionService` wrapping `ICrypto`
- Encrypts: IBAN, Kontoinhaber, Allergien
- Decrypt only when rendering for a user with appropriate permission
- Audit log shows `[verschlüsselt]` for encrypted field changes

**Soft deletion:**
- `deleted_at` timestamp + `status = 'geloescht'`
- On soft-delete: hard-delete banking, allergies, notes, addresses, phones, emails
- Retain: name, dates, Stufe history, Lager attendance, fee records
- Full DSGVO delete: remove everything including the retained fields

### 5.4 Frontend Architecture

- **Vue 3 + Composition API**
- **Pinia** for state management
- **@nextcloud/vue** component library (NcButton, NcModal, NcTextField, NcSelect, NcDateTimePicker, NcAppNavigation, NcAppContent, etc.)
- **@nextcloud/axios** for API calls (handles CSRF)
- **@nextcloud/router** for URL generation

**Key UI flows:**
1. **Member list** — table with global search + saved query sidebar. Click row → detail view
2. **Member detail** — tabbed: Persönlich, Familie, Beitrag, Lager, Verletzungen, Dateien, Verlauf
3. **Family view** — family card with all linked members, banking details
4. **Fee overview** — year selector, table with amounts/paid status, export
5. **Lager list** — past and upcoming, filterable by year/Stufe. Click → detail with participants, files
6. **Query builder** — visual filter builder with AND/OR nesting, save/load queries
7. **Reports** — choose type, apply filters, preview, download PDF/CSV (with encryption prompt for sensitive data)
8. **Import wizard** — upload → column mapping → preview → confirm
9. **Milestones** — upcoming Jubiläen list
10. **Settings** — Stufen, fee rules (DSL editor), file paths, user permissions
11. **Audit log** — searchable/filterable log table (Admin only)

### 5.5 Recommended Dev Sequence

Build in this order for a usable system at each stage:

1. **Skeleton** — NC app scaffold, empty Vue app loads in Nextcloud, DB migrations run
2. **Members CRUD** — create/read/update members with polished form UI, table view
3. **Search & basic filter** — global search, preset quick filters
4. **Families** — family CRUD, link members, shared banking (encrypted)
5. **Stufen** — configurable list, assign to members, history tracking
6. **Permissions** — per-user access control, banking visibility flag
7. **Audit log** — full change tracking, viewer UI
8. **Fee engine** — configurable rules (DSL), batch calculation, overview UI
9. **Reports** — PDF + CSV generation, encrypted exports
10. **Calendar sync** — birthday events for Mitglieder (not Erziehungsberechtigte)
11. **Contacts sync** — address book for all active members
12. **Files integration** — member folders, configurable paths, inline viewer
13. **Saved queries** — visual query builder, save/load/share
14. **Lagertracking** — Lager CRUD, participants, files, per-member history
15. **Injury tracking** — log entries, per-member and per-Lager views
16. **Import wizard** — CSV/Excel import with mapping and duplicate detection
17. **Milestones** — Jubiläen detection and view
18. **Soft deletion & DSGVO** — soft-delete logic, data export, hard-delete
19. **Stufe suggestions** — advisory notifications based on age ranges
20. **Security hardening** — pentest, dependency audit, rate limiting, final review

### 5.6 Useful Nextcloud APIs

| Need | API |
|---|---|
| DB queries | `\OCP\AppFramework\Db\QBMapper` |
| DB migrations | `\OCP\Migration\SimpleMigrationStep` |
| Current user | `\OCP\IUserSession` |
| Background jobs | `\OCP\BackgroundJob\TimedJob` |
| Calendar | `\OCA\DAV\CalDAV\CalDavBackend` |
| Contacts | `\OCA\DAV\CardDAV\CardDavBackend` |
| Files | `\OCP\Files\IRootFolder` |
| Encryption | `\OCP\Security\ICrypto` |
| Logging | `\Psr\Log\LoggerInterface` |
| App config | `\OCP\IConfig` |
| CSRF | `@NoCSRFRequired` annotation |

**Docs:** https://docs.nextcloud.com/server/latest/developer_manual/  
**@nextcloud/vue:** https://nextcloud-vue-components.netlify.app/

### 5.7 Resolved Decisions

| # | Question | Decision |
|---|---|---|
| 1 | Family discount rules | Configurable via DSL/rule table |
| 2 | Proration / refund on exit | No proration, no refund |
| 3 | App name | `mitgliederverwaltung` |
| 4 | Stufe auto-advance | Manual only, with advisory suggestions |
| 5 | Multiple Vereine | One Verein per NC instance |
| 6 | Notifications | Not for now |
| 7 | Backup | Standard NC/DB backup; exports encrypted |
| 8 | Phone format | E.164 enforced (+49...) |
| 9 | Multiple addresses | Yes, multiple per member |
| 10 | Archiving | Soft-delete: retain name/dates, hard-delete sensitive data |
