From fbd3b1cb30010d6c95a396c3a0189711ff163cc2 Mon Sep 17 00:00:00 2001 From: shahondin1624 Date: Tue, 7 Apr 2026 10:56:26 +0200 Subject: [PATCH] Initial scaffold for Mitgliederverwaltung Nextcloud app Directory structure, minimal skeleton files (info.xml, Application.php, routes.php, templates, Vue entry point), and requirements doc. No implementation yet. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 23 ++ appinfo/info.xml | 24 ++ appinfo/routes.php | 9 + composer.json | 19 + css/.gitkeep | 0 docs/requirements.md | 722 ++++++++++++++++++++++++++++++++++++ img/.gitkeep | 0 lib/AppInfo/Application.php | 24 ++ lib/Controller/.gitkeep | 0 lib/Db/.gitkeep | 0 lib/Migration/.gitkeep | 0 lib/Service/.gitkeep | 0 package.json | 10 + src/components/.gitkeep | 0 src/main.js | 1 + src/stores/.gitkeep | 0 src/views/.gitkeep | 0 templates/index.php | 14 + tests/Integration/.gitkeep | 0 tests/Unit/.gitkeep | 0 20 files changed, 846 insertions(+) create mode 100644 .gitignore create mode 100644 appinfo/info.xml create mode 100644 appinfo/routes.php create mode 100644 composer.json create mode 100644 css/.gitkeep create mode 100644 docs/requirements.md create mode 100644 img/.gitkeep create mode 100644 lib/AppInfo/Application.php create mode 100644 lib/Controller/.gitkeep create mode 100644 lib/Db/.gitkeep create mode 100644 lib/Migration/.gitkeep create mode 100644 lib/Service/.gitkeep create mode 100644 package.json create mode 100644 src/components/.gitkeep create mode 100644 src/main.js create mode 100644 src/stores/.gitkeep create mode 100644 src/views/.gitkeep create mode 100644 templates/index.php create mode 100644 tests/Integration/.gitkeep create mode 100644 tests/Unit/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b129e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Dependencies +/vendor/ +/node_modules/ + +# Build output +/js/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Composer +composer.lock + +# npm +package-lock.json diff --git a/appinfo/info.xml b/appinfo/info.xml new file mode 100644 index 0000000..075cebb --- /dev/null +++ b/appinfo/info.xml @@ -0,0 +1,24 @@ + + + mitgliederverwaltung + Mitgliederverwaltung + Mitgliederverwaltung fuer Pfadfindervereine + + 0.0.1 + agpl + shahondin1624 + Mitgliederverwaltung + organization + + + + mysql + + + + Mitgliederverwaltung + mitgliederverwaltung.page.index + + + diff --git a/appinfo/routes.php b/appinfo/routes.php new file mode 100644 index 0000000..f121d8b --- /dev/null +++ b/appinfo/routes.php @@ -0,0 +1,9 @@ + [ + ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], + ], +]; diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1675895 --- /dev/null +++ b/composer.json @@ -0,0 +1,19 @@ +{ + "name": "shahondin1624/mitgliederverwaltung", + "description": "Nextcloud app for managing scout group members", + "type": "project", + "license": "AGPL-3.0-or-later", + "require": { + "php": ">=8.1" + }, + "autoload": { + "psr-4": { + "OCA\\Mitgliederverwaltung\\": "lib/" + } + }, + "autoload-dev": { + "psr-4": { + "OCA\\Mitgliederverwaltung\\Tests\\": "tests/" + } + } +} diff --git a/css/.gitkeep b/css/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/requirements.md b/docs/requirements.md new file mode 100644 index 0000000..72dc21d --- /dev/null +++ b/docs/requirements.md @@ -0,0 +1,722 @@ +# Mitgliederverwaltung für Pfadfindervereine — Requirements & Architecture + +**Version:** 2.0 +**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 | +| 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 (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, 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, + 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 | diff --git a/img/.gitkeep b/img/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php new file mode 100644 index 0000000..35c291e --- /dev/null +++ b/lib/AppInfo/Application.php @@ -0,0 +1,24 @@ + + +
+
+
diff --git a/tests/Integration/.gitkeep b/tests/Integration/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/Unit/.gitkeep b/tests/Unit/.gitkeep new file mode 100644 index 0000000..e69de29