trim((string)($source['q'] ?? '')), 'status' => $status, 'zugang' => $zugang, 'paypal' => $paypal, ]; } function mitglieder_build_filter_query(array $filters): string { $params = []; foreach (['q', 'status', 'zugang', 'paypal'] as $key) { $value = (string)($filters[$key] ?? ''); if ($value !== '' && !in_array($value, ['alle'], true)) { $params[$key] = $value; } } return http_build_query($params); } function mitglieder_filter_fields(array $filters): string { return '' . '' . '' . ''; } function mitglieder_status_badge(bool $active): string { return '' . ($active ? 'Aktiv' : 'Inaktiv') . ''; } function mitglieder_access_label(array $mitglied, array $rollenLabels): string { if ($mitglied['membership_status'] === 'active') { return $rollenLabels[$mitglied['role']] ?? (string)$mitglied['role']; } return $mitglied['membership_status'] === 'revoked' ? 'Zugang entzogen' : 'Kein Zugang'; } if($hasAccess){ $meldung = null; $fehler = null; $einladungslink = null; $bearbeitenId = null; $actorUserId = $saasUser['user_id'] ?? null; $filterSource = $_SERVER['REQUEST_METHOD'] === 'POST' ? [ 'q' => $_POST['filter_q'] ?? '', 'status' => $_POST['filter_status'] ?? 'alle', 'zugang' => $_POST['filter_zugang'] ?? 'alle', 'paypal' => $_POST['filter_paypal'] ?? 'alle', ] : $_GET; $filter = mitglieder_parse_filters($filterSource); $filterQuery = mitglieder_build_filter_query($filter); $filterSuffix = $filterQuery !== '' ? '?' . $filterQuery : ''; if (isset($_SESSION['flash_mitglieder']) && is_array($_SESSION['flash_mitglieder'])) { $flash = $_SESSION['flash_mitglieder']; unset($_SESSION['flash_mitglieder']); $meldung = isset($flash['meldung']) ? (string)$flash['meldung'] : null; $fehler = isset($flash['fehler']) ? (string)$flash['fehler'] : null; $einladungslink = isset($flash['einladungslink']) ? (string)$flash['einladungslink'] : null; } if ($_SERVER["REQUEST_METHOD"] == "POST") { $aktion = $_POST["aktion"] ?? ''; if ($aktion === 'bearbeiten') { $bearbeitenId = (int)$_POST["mitgliedID"]; } elseif ($aktion === 'anlegen') { $name = trim((string)($_POST["name"] ?? '')); $email = trim((string)($_POST["email"] ?? '')); $paypalname = trim((string)($_POST["paypalname"] ?? '')); $aktiv = isset($_POST["aktiv"]); if ($name === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { $fehler = 'Bitte einen Namen und eine gültige E-Mail-Adresse angeben.'; } elseif ($aktiv && !billing_has_capacity_for($pdo, $tenantId)) { $fehler = billing_capacity_error_message($pdo, $tenantId); } else { try { $neueId = ledger_create_participant($pdo, $tenantId, $name, $email, $paypalname, $aktiv); app_audit_log($pdo, $tenantId, $actorUserId, 'participant.created', 'participant', $neueId, ['name' => $name, 'email' => $email]); $meldung = 'Mitglied wurde angelegt.'; } catch (Throwable $e) { $fehler = 'Das Mitglied konnte nicht angelegt werden (E-Mail eventuell schon vergeben).'; } } } elseif ($aktion === 'bearbeitenspeichern') { $participantId = (int)$_POST["mitgliedID"]; $name = trim((string)($_POST["name"] ?? '')); $email = trim((string)($_POST["email"] ?? '')); $paypalname = trim((string)($_POST["paypalname"] ?? '')); $aktiv = isset($_POST["aktiv"]); $warAktiv = false; foreach (ledger_fetch_participants_for_admin($pdo, $tenantId) as $m) { if ($m['participant_id'] === $participantId) { $warAktiv = (bool)$m['active']; break; } } if ($name === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { $fehler = 'Bitte einen Namen und eine gültige E-Mail-Adresse angeben.'; } elseif ($aktiv && !$warAktiv && !billing_has_capacity_for($pdo, $tenantId)) { $fehler = billing_capacity_error_message($pdo, $tenantId); } elseif (ledger_update_participant($pdo, $tenantId, $participantId, $name, $email, $paypalname, $aktiv)) { app_audit_log($pdo, $tenantId, $actorUserId, 'participant.updated', 'participant', $participantId, ['name' => $name, 'email' => $email]); $meldung = 'Mitglied wurde gespeichert.'; } else { $fehler = 'Das Mitglied konnte nicht gespeichert werden.'; } } elseif ($aktion === 'aktivieren' || $aktion === 'deaktivieren') { $participantId = (int)$_POST["mitgliedID"]; if ($aktion === 'aktivieren' && !billing_has_capacity_for($pdo, $tenantId)) { $fehler = billing_capacity_error_message($pdo, $tenantId); } elseif (ledger_set_participant_active($pdo, $tenantId, $participantId, $aktion === 'aktivieren')) { app_audit_log($pdo, $tenantId, $actorUserId, $aktion === 'aktivieren' ? 'participant.activated' : 'participant.deactivated', 'participant', $participantId); $meldung = $aktion === 'aktivieren' ? 'Mitglied wurde aktiviert.' : 'Mitglied wurde deaktiviert.'; } else { $fehler = 'Der Status konnte nicht geändert werden.'; } } elseif ($aktion === 'zugang_gewaehren') { $participantId = (int)$_POST["mitgliedID"]; $rolle = (string)($_POST["rolle"] ?? ''); // Einladungen legen fremde Konten an und verschicken Mails nach // aussen. Wer die eigene Adresse noch nicht bestaetigt hat, kann // das nicht im Namen eines womoeglich fremden Postfachs tun. if (!saas_email_verified($saasUser)) { $ergebnis = ['ok' => false, 'errors' => [ 'Bevor du Mitglieder einladen kannst, muss deine eigene E-Mail-Adresse bestätigt sein. Den Link kannst du unter "Kundenkonto" erneut anfordern.', ]]; } else { $ergebnis = saas_grant_participant_access($pdo, $tenantId, $participantId, $rolle); } if ($ergebnis['ok']) { $einladendeTenantSettings = saas_fetch_tenant_settings($pdo, $tenantId); $mailErgebnis = saas_send_invite_mail( $ergebnis['user_email'], $einladendeTenantSettings['name'] ?? 'Kaffeeliste', $rolle, $ergebnis['token'] ); app_audit_log($pdo, $tenantId, $actorUserId, 'participant.access_granted', 'participant', $participantId, ['role' => $rolle]); $meldung = 'Zugang wurde gewährt, Einladung wurde verschickt.'; if (saas_should_show_auth_links()) { $einladungslink = saas_app_url('passwort-zuruecksetzen.php?token=' . urlencode($ergebnis['token'])); } } else { $fehler = implode(' ', $ergebnis['errors']); } } elseif ($aktion === 'zugang_entziehen') { $participantId = (int)$_POST["mitgliedID"]; $ergebnis = saas_revoke_participant_access($pdo, $tenantId, $participantId); if ($ergebnis['ok']) { app_audit_log($pdo, $tenantId, $actorUserId, 'participant.access_revoked', 'participant', $participantId); $meldung = 'Zugang wurde entzogen.'; } else { $fehler = implode(' ', $ergebnis['errors']); } } elseif ($aktion === 'anonymisieren') { $participantId = (int)$_POST["mitgliedID"]; $mitgliedRolle = null; foreach (ledger_fetch_participants_for_admin($pdo, $tenantId) as $m) { if ($m['participant_id'] === $participantId) { $mitgliedRolle = $m['role']; break; } } if ($mitgliedRolle === 'owner') { $fehler = 'Der Inhaber kann nicht anonymisiert werden.'; } elseif (ledger_anonymize_participant($pdo, $tenantId, $participantId)) { app_audit_log($pdo, $tenantId, $actorUserId, 'participant.anonymized', 'participant', $participantId); $meldung = 'Mitglied wurde anonymisiert. Name und E-Mail sind entfernt, Buchungen bleiben für die Kassenführung erhalten.'; } else { $fehler = 'Das Mitglied konnte nicht anonymisiert werden.'; } } elseif ($aktion === 'csv_import') { $file = $_FILES['mitglieder_csv'] ?? null; if ($file === null || ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { $fehler = 'Bitte eine CSV-Datei auswählen.'; } elseif (($file['size'] ?? 0) <= 0 || $file['size'] > 2 * 1024 * 1024) { $fehler = 'Die CSV-Datei ist leer oder größer als 2 MB.'; } elseif (strtolower(pathinfo((string)$file['name'], PATHINFO_EXTENSION)) !== 'csv') { $fehler = 'Es sind nur CSV-Dateien erlaubt.'; } elseif (!is_uploaded_file((string)$file['tmp_name'])) { $fehler = 'Die hochgeladene Datei konnte nicht verifiziert werden.'; } else { try { $zeilen = imports_parse_member_csv((string)$file['tmp_name']); $angelegt = 0; $uebersprungen = 0; $inaktivWegenKapazitaet = 0; $fehlerhaft = 0; foreach ($zeilen as $z) { if ($z['name'] === '' || !filter_var($z['email'], FILTER_VALIDATE_EMAIL)) { $fehlerhaft++; continue; } $aktiv = $z['active']; if ($aktiv && !billing_has_capacity_for($pdo, $tenantId)) { $aktiv = false; $inaktivWegenKapazitaet++; } try { $neueId = ledger_create_participant($pdo, $tenantId, $z['name'], $z['email'], $z['paypal_name'] !== '' ? $z['paypal_name'] : null, $aktiv); app_audit_log($pdo, $tenantId, $actorUserId, 'participant.created', 'participant', $neueId, ['name' => $z['name'], 'email' => $z['email'], 'quelle' => 'csv_import']); $angelegt++; } catch (Throwable $e) { $uebersprungen++; } } $teile = ["{$angelegt} angelegt"]; if ($uebersprungen > 0) { $teile[] = "{$uebersprungen} übersprungen (bereits vorhanden)"; } if ($inaktivWegenKapazitaet > 0) { $teile[] = "{$inaktivWegenKapazitaet} als inaktiv angelegt (Tarif-Kapazität erschöpft)"; } if ($fehlerhaft > 0) { $teile[] = "{$fehlerhaft} fehlerhaft (Name/E-Mail ungültig)"; } $meldung = 'CSV-Import: ' . implode(', ', $teile) . '.'; } catch (Throwable $e) { $fehler = 'Die CSV-Datei konnte nicht verarbeitet werden.'; } } } if ($aktion !== 'bearbeiten') { $_SESSION['flash_mitglieder'] = [ 'meldung' => $meldung, 'fehler' => $fehler, 'einladungslink' => $einladungslink, ]; header('Location: mitarbeiterverwalten.php' . $filterSuffix); exit; } } $mitglieder = ledger_fetch_participants_for_admin($pdo, $tenantId); $billingInfo = billing_fetch_or_init($pdo, $tenantId); $billingPlan = billing_plans()[$billingInfo['plan_code']] ?? null; $aktiveMitgliederAnzahl = billing_count_active_participants($pdo, $tenantId); $rollenLabels = ['owner' => 'Inhaber', 'admin' => 'Administrator', 'treasurer' => 'Kassenwart', 'member' => 'Mitglied', 'viewer' => 'Betrachter']; $paypalNameCounts = []; foreach ($mitglieder as $mitglied) { $paypalNorm = imports_normalize_match_text((string)($mitglied['paypal_name'] ?? '')); if ($paypalNorm !== '') { $paypalNameCounts[$paypalNorm] = ($paypalNameCounts[$paypalNorm] ?? 0) + 1; } } $mitgliederStats = [ 'gesamt' => count($mitglieder), 'aktiv' => 0, 'inaktiv' => 0, 'mit_zugang' => 0, 'ohne_zugang' => 0, 'paypal_fehlt' => 0, 'paypal_doppelt' => 0, ]; $gefilterteMitglieder = []; foreach ($mitglieder as $mitglied) { $isActive = (bool)$mitglied['active']; $hasAccessAccount = $mitglied['membership_status'] === 'active'; $paypalName = trim((string)($mitglied['paypal_name'] ?? '')); $paypalNorm = imports_normalize_match_text($paypalName); $paypalDuplicate = $paypalNorm !== '' && ($paypalNameCounts[$paypalNorm] ?? 0) > 1; $mitgliederStats[$isActive ? 'aktiv' : 'inaktiv']++; $mitgliederStats[$hasAccessAccount ? 'mit_zugang' : 'ohne_zugang']++; if ($paypalName === '') { $mitgliederStats['paypal_fehlt']++; } if ($paypalDuplicate) { $mitgliederStats['paypal_doppelt']++; } $matches = true; $q = imports_normalize_match_text((string)$filter['q']); if ($q !== '') { $searchText = imports_normalize_match_text( (string)$mitglied['display_name'] . ' ' . (string)($mitglied['email'] ?? '') . ' ' . $paypalName ); $matches = str_contains($searchText, $q); } if ($matches && $filter['status'] === 'aktiv') { $matches = $isActive; } if ($matches && $filter['status'] === 'inaktiv') { $matches = !$isActive; } if ($matches && $filter['zugang'] === 'mit_zugang') { $matches = $hasAccessAccount; } if ($matches && $filter['zugang'] === 'ohne_zugang') { $matches = $mitglied['membership_status'] === null || $mitglied['membership_status'] === 'pending'; } if ($matches && $filter['zugang'] === 'entzogen') { $matches = $mitglied['membership_status'] === 'revoked'; } if ($matches && $filter['paypal'] === 'hinterlegt') { $matches = $paypalName !== ''; } if ($matches && $filter['paypal'] === 'fehlt') { $matches = $paypalName === ''; } if ($matches && $filter['paypal'] === 'doppelt') { $matches = $paypalDuplicate; } if ($matches) { $mitglied['paypal_duplicate'] = $paypalDuplicate; $gefilterteMitglieder[] = $mitglied; } } $bearbeitenMitglied = null; if ($bearbeitenId !== null) { foreach ($mitglieder as $m) { if ($m['participant_id'] === $bearbeitenId) { $bearbeitenMitglied = $m; break; } } } ?>

Mitglieder verwalten

Dev-Link zur Einladung:

Aktive Mitglieder: . = $billingPlan['max_participants']): ?> Limit erreicht – für weitere aktive Mitglieder ist ein Upgrade nötig.

Mitglieder
aktiv
mit Zugang
ohne PayPal-Name
PayPal-Konflikte

Bearbeiten von

">
>

Neues Mitglied anlegen

">

Mitglieder per CSV importieren

Lade zunächst die Vorlage herunter, trage eure Mitglieder ein (Name, E-Mail, optional PayPal-Name und „ja/nein" für aktiv) und lade die Datei anschließend wieder hoch. Bereits vorhandene E-Mail-Adressen werden übersprungen.

Vorlage herunterladen

" enctype="multipart/form-data">

Name und E-Mail dienen der Kaffeeliste und Benachrichtigungen. Ein Login-Zugang ist davon unabhängig und wird separat je Mitglied gewährt oder entzogen.

Zurücksetzen

von Mitgliedern angezeigt.

Name E-Mail PayPal-Name Aktiv Zugang Aktionen

Konflikt
Inhaber
" class="inline-admin-form">
" class="inline-admin-form">
" class="inline-admin-form">
  • ">
  • ">
    ">
  • " onsubmit="return confirm('Name und E-Mail dieses Mitglieds unwiderruflich entfernen? Buchungen bleiben erhalten.');">
Keine Mitglieder für diese Filter gefunden.
Sie haben keinen Zugang zu dieser Webseite"; } ?>