M5: Hinweise auf tenant-scoped Notices umziehen, Mitgliederzugang auf Rollen umstellen
Hinweise: - Neue Tabelle notices (tenant-scoped, Soft-Delete via deleted_at) loest die global unscoped kl_hinweise als aktive Datenquelle ab; Migration uebernimmt einmalig aktuell gueltige kl_hinweise-Eintraege fuer den Default-Mandanten. kl_hinweise bleibt als Golden-Master-Referenz stehen. - hinweise.php und der Banner in header.php sind tenant-scoped umgestellt. Mitgliederverwaltung: - mitarbeiterverwalten.php verwaltet jetzt participants (tenant-scoped) statt der global unscoped kl_Mitarbeiter-Tabelle als primaere Quelle. Das behebt nebenbei ein Mandanten-Datenleck: jeder SaaS-Mandant mit Owner/Admin-Rolle haette zuvor die komplette Default-Mandanten- Mitgliederliste sehen und bearbeiten koennen. - Fuer den Default-Mandanten bleibt Dual-Write nach kl_Mitarbeiter bestehen, damit stricheintragen.php/einzahlung.php weiter funktionieren; andere Mandanten werden rein participant-nativ verwaltet. - Die Legacy-Administrator-Checkbox ist raus. Stattdessen kann ein Admin je Mitglied unabhaengig von Name/E-Mail einen Login-Zugang mit Rolle (member/treasurer/admin) gewaehren oder entziehen (saas_grant_participant_access / saas_revoke_participant_access). Einladung laeuft ueber den bestehenden Passwort-Reset-Mechanismus, Entzug setzt die Mitgliedschaft auf revoked statt sie zu loeschen. - Kompletter Flow live getestet: anlegen, Zugang gewaehren, Einladungsmail, Passwort setzen, Login, Rollenschutz, Zugang entziehen, Login-Sperre. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+170
-23
@@ -319,34 +319,181 @@ function ledger_mirror_legacy_consumption(PDO $pdo, int $tenantId, int $legacyCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ledger_is_default_tenant(PDO $pdo, int $tenantId): bool
|
||||||
|
{
|
||||||
|
$tenant = ledger_fetch_default_tenant($pdo);
|
||||||
|
|
||||||
|
return $tenant !== null && (int)$tenant['id'] === $tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mirrors a single kl_Mitarbeiter row into participants, so member changes
|
* @return list<array{participant_id: int, display_name: string, email: ?string, email_norm: ?string, paypal_name: ?string, active: bool, legacy_mitarbeiter_id: ?int, user_id: ?int, role: ?string, membership_status: ?string}>
|
||||||
* made through the legacy admin UI are immediately visible to ledger-backed
|
|
||||||
* reads instead of waiting for the next scripts/backfill-default-tenant.php run.
|
|
||||||
*/
|
*/
|
||||||
function ledger_mirror_legacy_participant(PDO $pdo, int $tenantId, int $legacyMitarbeiterId): void
|
function ledger_fetch_participants_for_admin(PDO $pdo, int $tenantId): array
|
||||||
{
|
{
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
"INSERT INTO participants
|
"SELECT
|
||||||
(tenant_id, display_name, email, email_norm, paypal_name, active, legacy_mitarbeiter_id)
|
p.id AS participant_id,
|
||||||
SELECT
|
p.display_name,
|
||||||
?,
|
p.email,
|
||||||
NULLIF(TRIM(m.Name), ''),
|
p.email_norm,
|
||||||
NULLIF(TRIM(m.Email), ''),
|
p.paypal_name,
|
||||||
NULLIF(LOWER(TRIM(m.Email)), ''),
|
p.active,
|
||||||
NULLIF(TRIM(m.paypalname), ''),
|
p.legacy_mitarbeiter_id,
|
||||||
m.aktiv,
|
p.user_id,
|
||||||
m.MitarbeiterID
|
tm.role,
|
||||||
FROM kl_Mitarbeiter m
|
tm.status AS membership_status
|
||||||
WHERE m.MitarbeiterID = ?
|
FROM participants p
|
||||||
ON DUPLICATE KEY UPDATE
|
LEFT JOIN tenant_memberships tm
|
||||||
display_name = COALESCE(VALUES(display_name), participants.display_name),
|
ON tm.tenant_id = p.tenant_id
|
||||||
email = VALUES(email),
|
AND tm.user_id = p.user_id
|
||||||
email_norm = VALUES(email_norm),
|
WHERE p.tenant_id = ?
|
||||||
paypal_name = VALUES(paypal_name),
|
ORDER BY p.display_name, p.id"
|
||||||
active = VALUES(active)"
|
|
||||||
);
|
);
|
||||||
$stmt->execute([$tenantId, $legacyMitarbeiterId]);
|
$stmt->execute([$tenantId]);
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
while ($row = $stmt->fetch()) {
|
||||||
|
$rows[] = [
|
||||||
|
'participant_id' => (int)$row['participant_id'],
|
||||||
|
'display_name' => (string)$row['display_name'],
|
||||||
|
'email' => $row['email'] !== null ? (string)$row['email'] : null,
|
||||||
|
'email_norm' => $row['email_norm'] !== null ? (string)$row['email_norm'] : null,
|
||||||
|
'paypal_name' => $row['paypal_name'] !== null ? (string)$row['paypal_name'] : null,
|
||||||
|
'active' => (int)$row['active'] === 1,
|
||||||
|
'legacy_mitarbeiter_id' => $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null,
|
||||||
|
'user_id' => $row['user_id'] !== null ? (int)$row['user_id'] : null,
|
||||||
|
'role' => $row['role'] !== null ? (string)$row['role'] : null,
|
||||||
|
'membership_status' => $row['membership_status'] !== null ? (string)$row['membership_status'] : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a tenant-scoped participant. For the default tenant, a matching
|
||||||
|
* kl_Mitarbeiter row is dual-written too, so the still-legacy bulk entry
|
||||||
|
* pages (stricheintragen.php, einzahlung.php) keep listing the member; other
|
||||||
|
* tenants have no legacy shadow table and get a participants-only row.
|
||||||
|
*
|
||||||
|
* @throws Throwable on constraint violations (e.g. duplicate email)
|
||||||
|
*/
|
||||||
|
function ledger_create_participant(PDO $pdo, int $tenantId, string $displayName, string $email, ?string $paypalName, bool $active): int
|
||||||
|
{
|
||||||
|
$displayName = trim($displayName);
|
||||||
|
$email = trim($email);
|
||||||
|
$emailNorm = strtolower($email);
|
||||||
|
$paypalName = $paypalName !== null ? trim($paypalName) : null;
|
||||||
|
$paypalName = $paypalName !== '' ? $paypalName : null;
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
try {
|
||||||
|
$legacyMitarbeiterId = null;
|
||||||
|
if (ledger_is_default_tenant($pdo, $tenantId)) {
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'INSERT INTO kl_Mitarbeiter (Name, Email, paypalname, aktiv, admin) VALUES (?, ?, ?, ?, 0)'
|
||||||
|
);
|
||||||
|
$stmt->execute([$displayName, $email, $paypalName, $active ? 1 : 0]);
|
||||||
|
$legacyMitarbeiterId = (int)$pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'INSERT INTO participants
|
||||||
|
(tenant_id, display_name, email, email_norm, paypal_name, active, legacy_mitarbeiter_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||||
|
);
|
||||||
|
$stmt->execute([$tenantId, $displayName, $email, $emailNorm, $paypalName, $active ? 1 : 0, $legacyMitarbeiterId]);
|
||||||
|
$participantId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
return $participantId;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ledger_update_participant(PDO $pdo, int $tenantId, int $participantId, string $displayName, string $email, ?string $paypalName, bool $active): bool
|
||||||
|
{
|
||||||
|
$displayName = trim($displayName);
|
||||||
|
$email = trim($email);
|
||||||
|
$emailNorm = strtolower($email);
|
||||||
|
$paypalName = $paypalName !== null ? trim($paypalName) : null;
|
||||||
|
$paypalName = $paypalName !== '' ? $paypalName : null;
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT legacy_mitarbeiter_id FROM participants WHERE id = ? AND tenant_id = ?');
|
||||||
|
$stmt->execute([$participantId, $tenantId]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
if ($row === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$legacyMitarbeiterId = $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
if ($legacyMitarbeiterId !== null) {
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'UPDATE kl_Mitarbeiter SET Name = ?, Email = ?, paypalname = ?, aktiv = ? WHERE MitarbeiterID = ?'
|
||||||
|
);
|
||||||
|
$stmt->execute([$displayName, $email, $paypalName, $active ? 1 : 0, $legacyMitarbeiterId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'UPDATE participants
|
||||||
|
SET display_name = ?, email = ?, email_norm = ?, paypal_name = ?, active = ?
|
||||||
|
WHERE id = ? AND tenant_id = ?'
|
||||||
|
);
|
||||||
|
$stmt->execute([$displayName, $email, $emailNorm, $paypalName, $active ? 1 : 0, $participantId, $tenantId]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ledger_set_participant_active(PDO $pdo, int $tenantId, int $participantId, bool $active): bool
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare('SELECT legacy_mitarbeiter_id FROM participants WHERE id = ? AND tenant_id = ?');
|
||||||
|
$stmt->execute([$participantId, $tenantId]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
if ($row === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$legacyMitarbeiterId = $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
if ($legacyMitarbeiterId !== null) {
|
||||||
|
$pdo->prepare('UPDATE kl_Mitarbeiter SET aktiv = ? WHERE MitarbeiterID = ?')
|
||||||
|
->execute([$active ? 1 : 0, $legacyMitarbeiterId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->prepare('UPDATE participants SET active = ? WHERE id = ? AND tenant_id = ?')
|
||||||
|
->execute([$active ? 1 : 0, $participantId, $tenantId]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ledger_mirror_legacy_payment(PDO $pdo, int $tenantId, int $legacyPaymentId): void
|
function ledger_mirror_legacy_payment(PDO $pdo, int $tenantId, int $legacyPaymentId): void
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{id: int, message: string, valid_from: string, valid_until: string}|null
|
||||||
|
*/
|
||||||
|
function notices_fetch_active(PDO $pdo, int $tenantId): ?array
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT id, message, valid_from, valid_until
|
||||||
|
FROM notices
|
||||||
|
WHERE tenant_id = ?
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
AND valid_from <= NOW()
|
||||||
|
AND valid_until >= NOW()
|
||||||
|
ORDER BY valid_until ASC
|
||||||
|
LIMIT 1"
|
||||||
|
);
|
||||||
|
$stmt->execute([$tenantId]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
|
||||||
|
return $row !== false ? $row : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{id: int, message: string, valid_from: string, valid_until: string}>
|
||||||
|
*/
|
||||||
|
function notices_fetch_all(PDO $pdo, int $tenantId): array
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT id, message, valid_from, valid_until
|
||||||
|
FROM notices
|
||||||
|
WHERE tenant_id = ?
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
ORDER BY valid_until DESC"
|
||||||
|
);
|
||||||
|
$stmt->execute([$tenantId]);
|
||||||
|
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function notices_create(PDO $pdo, int $tenantId, string $message, string $validUntil, ?int $createdByUserId): bool
|
||||||
|
{
|
||||||
|
$message = trim($message);
|
||||||
|
if ($message === '' || $validUntil === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'INSERT INTO notices (tenant_id, message, valid_from, valid_until, created_by_user_id)
|
||||||
|
VALUES (?, ?, NOW(), ?, ?)'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $stmt->execute([$tenantId, $message, $validUntil, $createdByUserId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notices_soft_delete(PDO $pdo, int $tenantId, int $noticeId): bool
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'UPDATE notices
|
||||||
|
SET deleted_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
AND tenant_id = ?
|
||||||
|
AND deleted_at IS NULL'
|
||||||
|
);
|
||||||
|
$stmt->execute([$noticeId, $tenantId]);
|
||||||
|
|
||||||
|
return $stmt->rowCount() > 0;
|
||||||
|
}
|
||||||
@@ -848,6 +848,132 @@ function saas_authenticate(PDO $pdo, string $email, string $password, string $te
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roles an admin can hand out through the member management UI. "owner" is
|
||||||
|
* deliberately excluded here; it is only ever set during registration.
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
function saas_grantable_roles(): array
|
||||||
|
{
|
||||||
|
return ['member', 'treasurer', 'admin'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grants (or updates) tenant access for an existing participant: creates the
|
||||||
|
* login account if needed, links it to the participant, and (re)activates
|
||||||
|
* the tenant membership with the given role. The participant's name/email
|
||||||
|
* stay the notification identity regardless of login state.
|
||||||
|
*
|
||||||
|
* @return array{ok: bool, errors: list<string>, token?: string, user_email?: string}
|
||||||
|
*/
|
||||||
|
function saas_grant_participant_access(PDO $pdo, int $tenantId, int $participantId, string $role): array
|
||||||
|
{
|
||||||
|
if (!in_array($role, saas_grantable_roles(), true)) {
|
||||||
|
return ['ok' => false, 'errors' => ['Ungültige Rolle.']];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'SELECT id, display_name, email, email_norm
|
||||||
|
FROM participants
|
||||||
|
WHERE id = ? AND tenant_id = ?'
|
||||||
|
);
|
||||||
|
$stmt->execute([$participantId, $tenantId]);
|
||||||
|
$participant = $stmt->fetch();
|
||||||
|
if ($participant === false) {
|
||||||
|
return ['ok' => false, 'errors' => ['Teilnehmer wurde nicht gefunden.']];
|
||||||
|
}
|
||||||
|
|
||||||
|
$emailNorm = (string)($participant['email_norm'] ?? '');
|
||||||
|
if ($emailNorm === '' || !filter_var($participant['email'], FILTER_VALIDATE_EMAIL)) {
|
||||||
|
return ['ok' => false, 'errors' => ['Für den Zugang wird eine gültige E-Mail-Adresse benötigt.']];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT id, status FROM users WHERE email_norm = ? LIMIT 1');
|
||||||
|
$stmt->execute([$emailNorm]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($user === false) {
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'INSERT INTO users (email, email_norm, display_name, status) VALUES (?, ?, ?, ?)'
|
||||||
|
);
|
||||||
|
$stmt->execute([$participant['email'], $emailNorm, $participant['display_name'], 'active']);
|
||||||
|
$userId = (int)$pdo->lastInsertId();
|
||||||
|
} elseif ((string)$user['status'] !== 'active') {
|
||||||
|
$pdo->rollBack();
|
||||||
|
|
||||||
|
return ['ok' => false, 'errors' => ['Dieses Benutzerkonto ist nicht aktiv.']];
|
||||||
|
} else {
|
||||||
|
$userId = (int)$user['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'INSERT INTO tenant_memberships (tenant_id, user_id, role, status, invited_at, joined_at)
|
||||||
|
VALUES (?, ?, ?, ?, NOW(), NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
role = VALUES(role),
|
||||||
|
status = VALUES(status),
|
||||||
|
invited_at = COALESCE(tenant_memberships.invited_at, VALUES(invited_at)),
|
||||||
|
joined_at = COALESCE(tenant_memberships.joined_at, VALUES(joined_at))'
|
||||||
|
);
|
||||||
|
$stmt->execute([$tenantId, $userId, $role, 'active']);
|
||||||
|
|
||||||
|
$pdo->prepare('UPDATE participants SET user_id = ? WHERE id = ? AND tenant_id = ?')
|
||||||
|
->execute([$userId, $participantId, $tenantId]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['ok' => false, 'errors' => ['Der Zugang konnte nicht angelegt werden.']];
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = saas_create_auth_token($pdo, $userId, 'password_reset', $tenantId, 1440);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'errors' => [],
|
||||||
|
'token' => $token['token'],
|
||||||
|
'user_email' => (string)$participant['email'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revokes tenant access without touching the participant's name/email or
|
||||||
|
* deleting the membership row, so the grant can be reinstated later and the
|
||||||
|
* history stays auditable. Owners cannot be revoked here.
|
||||||
|
*
|
||||||
|
* @return array{ok: bool, errors: list<string>}
|
||||||
|
*/
|
||||||
|
function saas_revoke_participant_access(PDO $pdo, int $tenantId, int $participantId): array
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare('SELECT user_id FROM participants WHERE id = ? AND tenant_id = ?');
|
||||||
|
$stmt->execute([$participantId, $tenantId]);
|
||||||
|
$participant = $stmt->fetch();
|
||||||
|
if ($participant === false || $participant['user_id'] === null) {
|
||||||
|
return ['ok' => true, 'errors' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = (int)$participant['user_id'];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT role FROM tenant_memberships WHERE tenant_id = ? AND user_id = ?');
|
||||||
|
$stmt->execute([$tenantId, $userId]);
|
||||||
|
$membership = $stmt->fetch();
|
||||||
|
if ($membership !== false && (string)$membership['role'] === 'owner') {
|
||||||
|
return ['ok' => false, 'errors' => ['Dem Inhaber kann der Zugang nicht entzogen werden.']];
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->prepare("UPDATE tenant_memberships SET status = 'revoked' WHERE tenant_id = ? AND user_id = ?")
|
||||||
|
->execute([$tenantId, $userId]);
|
||||||
|
|
||||||
|
return ['ok' => true, 'errors' => []];
|
||||||
|
}
|
||||||
|
|
||||||
function saas_register_tenant_owner(PDO $pdo, array $input): array
|
function saas_register_tenant_owner(PDO $pdo, array $input): array
|
||||||
{
|
{
|
||||||
$tenantName = trim((string)($input['tenant_name'] ?? ''));
|
$tenantName = trim((string)($input['tenant_name'] ?? ''));
|
||||||
|
|||||||
@@ -131,6 +131,26 @@ function saas_send_password_reset_mail(string $to, string $token): array
|
|||||||
return saas_send_mail($to, 'Kaffeeliste Passwort zurücksetzen', $body);
|
return saas_send_mail($to, 'Kaffeeliste Passwort zurücksetzen', $body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saas_send_invite_mail(string $to, string $tenantName, string $role, string $token): array
|
||||||
|
{
|
||||||
|
$link = saas_app_url('passwort-zuruecksetzen.php?token=' . urlencode($token));
|
||||||
|
$body = implode("\n", [
|
||||||
|
'Hallo,',
|
||||||
|
'',
|
||||||
|
"du wurdest für \"{$tenantName}\" in der Kaffeeliste als {$role} eingeladen.",
|
||||||
|
'Öffne diesen Link, um ein Passwort zu setzen und dich anzumelden:',
|
||||||
|
'',
|
||||||
|
$link,
|
||||||
|
'',
|
||||||
|
'Der Link ist zeitlich begrenzt und kann nur einmal verwendet werden.',
|
||||||
|
'Falls du diese Einladung nicht erwartet hast, kannst du diese Nachricht ignorieren.',
|
||||||
|
'',
|
||||||
|
'Deine Kaffeeliste',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return saas_send_mail($to, 'Einladung zur Kaffeeliste', $body);
|
||||||
|
}
|
||||||
|
|
||||||
function saas_send_email_verification_mail(string $to, string $token): array
|
function saas_send_email_verification_mail(string $to, string $token): array
|
||||||
{
|
{
|
||||||
$link = saas_app_url('email-verifizieren.php?token=' . urlencode($token));
|
$link = saas_app_url('email-verifizieren.php?token=' . urlencode($token));
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS notices (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
tenant_id INT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
valid_from DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
valid_until DATETIME NOT NULL,
|
||||||
|
created_by_user_id INT NULL,
|
||||||
|
deleted_at DATETIME NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
KEY idx_notices_tenant_valid_until (tenant_id, valid_until),
|
||||||
|
KEY idx_notices_created_by_user (created_by_user_id),
|
||||||
|
CONSTRAINT fk_notices_tenant
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_notices_created_by_user
|
||||||
|
FOREIGN KEY (created_by_user_id) REFERENCES users(id)
|
||||||
|
ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- One-time carry-over so currently visible legacy banners are not lost when
|
||||||
|
-- hinweise.php switches from kl_hinweise to the tenant-scoped notices table.
|
||||||
|
-- kl_hinweise itself is left untouched; it stays as the golden-master
|
||||||
|
-- reference for the legacy system.
|
||||||
|
INSERT INTO notices (tenant_id, message, valid_from, valid_until)
|
||||||
|
SELECT t.id, h.nachricht, NOW(), h.gueltig_bis
|
||||||
|
FROM kl_hinweise h
|
||||||
|
JOIN tenants t ON t.slug = 'default'
|
||||||
|
WHERE h.gueltig_bis >= NOW();
|
||||||
+92
-3
@@ -167,13 +167,98 @@ Bewusst nicht umgesetzt:
|
|||||||
Admin-Rollen für neue SaaS-Nutzer laufen bis dahin weiter über
|
Admin-Rollen für neue SaaS-Nutzer laufen bis dahin weiter über
|
||||||
`scripts/backfill-default-tenant.php` oder die Registrierung.
|
`scripts/backfill-default-tenant.php` oder die Registrierung.
|
||||||
|
|
||||||
|
## Fortsetzung: Hinweise und rollenbasierter Zugang
|
||||||
|
|
||||||
|
Umgesetzte Dateien:
|
||||||
|
|
||||||
|
```text
|
||||||
|
database/migrations/0007_saas_notices.sql
|
||||||
|
app/notices.php
|
||||||
|
hinweise.php
|
||||||
|
header.php
|
||||||
|
mitarbeiterverwalten.php (grundlegend neu)
|
||||||
|
app/ledger.php (Participant-CRUD)
|
||||||
|
app/saas-auth.php (Zugangsvergabe/-entzug)
|
||||||
|
app/saas-mail.php (Einladungsmail)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hinweise auf Notices umgezogen
|
||||||
|
|
||||||
|
- Neue Tabelle `notices` (tenant-scoped, mit `deleted_at` für Soft-Delete)
|
||||||
|
ersetzt `kl_hinweise` als aktive Datenquelle. `kl_hinweise` bleibt
|
||||||
|
unangetastet als Golden-Master-Referenz.
|
||||||
|
- Die Migration übernimmt einmalig alle zum Migrationszeitpunkt noch
|
||||||
|
gültigen `kl_hinweise`-Einträge in `notices` des Default-Mandanten, damit
|
||||||
|
keine sichtbaren Banner verloren gehen.
|
||||||
|
- `hinweise.php` liest/schreibt jetzt ausschließlich `notices`, tenant-scoped
|
||||||
|
mit dem gleichen Rollen-/Legacy-Fallback-Muster wie andere Admin-Seiten.
|
||||||
|
Löschen ist ein Soft-Delete (`deleted_at`), kein Hard-Delete.
|
||||||
|
- `header.php` (Banner-Anzeige auf allen App-Seiten) löst den Mandanten
|
||||||
|
jetzt selbst auf (SaaS-Session oder Default-Tenant-Fallback) und zeigt den
|
||||||
|
aktuell gültigen Hinweis dieses Mandanten statt eines global-legacy
|
||||||
|
Hinweises.
|
||||||
|
|
||||||
|
### Mitgliederverwaltung: participant-nativ mit Rollen-Zugang
|
||||||
|
|
||||||
|
`mitarbeiterverwalten.php` wurde grundlegend umgebaut, nicht mehr additiv
|
||||||
|
gepatcht:
|
||||||
|
|
||||||
|
- Datenquelle ist jetzt `participants` (tenant-scoped), nicht mehr die
|
||||||
|
global unscoped `kl_Mitarbeiter`-Tabelle. Das behebt nebenbei ein
|
||||||
|
Datenleck: Da `kl_Mitarbeiter` keine `tenant_id` hat, hätte jeder
|
||||||
|
SaaS-Mandant mit Owner/Admin-Rolle über die vorherige Version dieser
|
||||||
|
Seite die komplette Mitgliederliste des Default-Mandanten sehen und
|
||||||
|
bearbeiten können.
|
||||||
|
- Für den Default-Mandanten wird weiterhin dual-write nach
|
||||||
|
`kl_Mitarbeiter` betrieben (`ledger_create_participant()`,
|
||||||
|
`ledger_update_participant()`, `ledger_set_participant_active()`), damit
|
||||||
|
`stricheintragen.php` und `einzahlung.php` (deren Mitarbeiter-Picker
|
||||||
|
weiterhin direkt aus `kl_Mitarbeiter` liest) neue Mitglieder sofort
|
||||||
|
anzeigen. Andere Mandanten haben keine Legacy-Schattentabelle und werden
|
||||||
|
rein participant-nativ verwaltet.
|
||||||
|
- Die Legacy-„Administrator"-Checkbox ist aus dem Anlegen-/Bearbeiten-
|
||||||
|
Formular entfernt. Stattdessen gibt es je Mitglied eine eigene
|
||||||
|
„Zugang"-Spalte: Rolle wählen (`member`, `treasurer`, `admin`) und
|
||||||
|
„Zugang gewähren", oder bei bestehendem Zugang Rolle ändern beziehungsweise
|
||||||
|
„Zugang entziehen". `owner` ist über diese UI nicht vergebbar oder
|
||||||
|
entziehbar (nur bei Registrierung gesetzt).
|
||||||
|
- Name und E-Mail eines Mitglieds (`participants.display_name`/`email`)
|
||||||
|
bleiben unabhängig vom Login: Ein Mitglied kann ohne jeden Zugang
|
||||||
|
existieren (nur für Kaffeeliste/Benachrichtigung), und ein Zugang kann
|
||||||
|
jederzeit gewährt oder entzogen werden, ohne den Mitgliedsdatensatz zu
|
||||||
|
berühren.
|
||||||
|
- Zugangsvergabe (`saas_grant_participant_access()`) legt bei Bedarf einen
|
||||||
|
`users`-Datensatz ohne Passwort an, setzt/aktualisiert die
|
||||||
|
`tenant_memberships`-Rolle und verknüpft `participants.user_id`.
|
||||||
|
Anschließend wird ein Einladungslink über den bestehenden
|
||||||
|
Passwort-Reset-Mechanismus verschickt (`saas_send_invite_mail()`, gleicher
|
||||||
|
Token-Typ `password_reset`, gleiche Zielseite
|
||||||
|
`passwort-zuruecksetzen.php` wie beim regulären Passwort-Reset).
|
||||||
|
- Zugangsentzug (`saas_revoke_participant_access()`) setzt die
|
||||||
|
`tenant_memberships`-Zeile auf `status = 'revoked'`, statt sie zu löschen
|
||||||
|
oder den `user_id`-Verweis zu entfernen. Der Zugang kann später erneut
|
||||||
|
gewährt werden, ohne den Account neu anzulegen. Der Login prüft bereits
|
||||||
|
überall auf `tm.status = 'active'`, wodurch ein entzogener Zugang sofort
|
||||||
|
wirkt.
|
||||||
|
- Live gegen die Dev-Datenbank getestet: Mitglied anlegen (inklusive
|
||||||
|
Dual-Write-Check), Zugang mit Rolle `treasurer` gewähren, Einladungsmail
|
||||||
|
geprüft, Passwort über den Einladungslink gesetzt, erfolgreicher Login,
|
||||||
|
Rollenschutz geprüft (kein Zugriff auf `mitarbeiterverwalten.php`, Zugriff
|
||||||
|
auf `kaffeeliste.php`), Zugang entzogen, anschließender Login-Versuch
|
||||||
|
korrekt mit „kein aktiver Mandant" abgelehnt.
|
||||||
|
|
||||||
## Noch offen
|
## Noch offen
|
||||||
|
|
||||||
- Eigene PayPal-/Zahlungsbereich als eigenständiger App-Screen (aktuell nur im
|
- Eigene PayPal-/Zahlungsbereich als eigenständiger App-Screen (aktuell nur im
|
||||||
Dashboard integriert).
|
Dashboard integriert).
|
||||||
- Einladungs-Flow, um bestehenden Teilnehmern nachträglich einen
|
- `stricheintragen.php` und `einzahlung.php` lesen ihre Mitarbeiter-Picker
|
||||||
Login-Account mit `tenant_memberships`-Rolle zuzuweisen.
|
weiterhin aus der global unscoped `kl_Mitarbeiter`-Tabelle. Für den
|
||||||
- Hinweise als tenant-spezifische Notices umsetzen.
|
Default-Mandanten funktioniert das unverändert; für jeden anderen Mandanten
|
||||||
|
ist die Liste faktisch leer beziehungsweise zeigt (nur lesend, Schreiben
|
||||||
|
schlägt dank Tenant-Scope in `ledger_mirror_legacy_*` sicher fehl) die
|
||||||
|
Namen der Default-Mandanten-Mitglieder an. Das ist ein bestehendes,
|
||||||
|
eigenständiges Scope-Thema für eine spätere Iteration, keine Regression
|
||||||
|
dieser Session.
|
||||||
- Export, Mail und Jahresprozesse bleiben M6-Themen.
|
- Export, Mail und Jahresprozesse bleiben M6-Themen.
|
||||||
|
|
||||||
## Aktueller Prüfstatus
|
## Aktueller Prüfstatus
|
||||||
@@ -187,3 +272,7 @@ Bewusst nicht umgesetzt:
|
|||||||
- Live-Test der Mitgliederverwaltung gegen die Dev-DB: Anlegen (inklusive
|
- Live-Test der Mitgliederverwaltung gegen die Dev-DB: Anlegen (inklusive
|
||||||
XSS-Payload-Check), Deaktivieren, participants-Spiegelung erfolgreich
|
XSS-Payload-Check), Deaktivieren, participants-Spiegelung erfolgreich
|
||||||
geprüft.
|
geprüft.
|
||||||
|
- Live-Test Hinweise/Notices: Carry-over-Migration, Anlegen mit HTML-Payload
|
||||||
|
(korrekt escaped), Soft-Delete, Banner-Anzeige auf Mandant geprüft.
|
||||||
|
- Live-Test Zugangsvergabe/-entzug: kompletter Flow von Einladung bis
|
||||||
|
Login-Sperre nach Entzug erfolgreich geprüft (siehe oben).
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ Nicht tun:
|
|||||||
| M2 | Technisches Fundament | Abgeschlossen: Migrationen, Bootstrap, Session, CSRF-Helper und Legacy-Schreibseitenschutz stehen |
|
| M2 | Technisches Fundament | Abgeschlossen: Migrationen, Bootstrap, Session, CSRF-Helper und Legacy-Schreibseitenschutz stehen |
|
||||||
| M3 | SaaS-Basis | Abgeschlossen: Tenants, User, Registrierung, Login, Rollen, Mail-Links und zentrale Mandantenauswahl funktionieren |
|
| M3 | SaaS-Basis | Abgeschlossen: Tenants, User, Registrierung, Login, Rollen, Mail-Links und zentrale Mandantenauswahl funktionieren |
|
||||||
| M4 | Datenmigration | Gestartet: Ledger-Tabelle, Legacy-Backfill, Paritätscheck, Ledger-Service und Preview sind umgesetzt |
|
| M4 | Datenmigration | Gestartet: Ledger-Tabelle, Legacy-Backfill, Paritätscheck, Ledger-Service und Preview sind umgesetzt |
|
||||||
| M5 | App-Kern | Weit fortgeschritten: Kernseiten lesen und schreiben tenant-sicher gegen das Ledger; offen sind Hinweise und ein eigener Zahlungs-Screen |
|
| M5 | App-Kern | Weit fortgeschritten: Kernseiten lesen und schreiben tenant-sicher gegen das Ledger, inklusive Hinweise und rollenbasiertem Zugang; offen ist ein eigener Zahlungs-Screen |
|
||||||
| M6 | Betriebsflows | Import, Export, Mail und Jahresprozesse sind auditierbar |
|
| M6 | Betriebsflows | Import, Export, Mail und Jahresprozesse sind auditierbar |
|
||||||
| M7 | Landingpage | Public-Seite und Auth-Seiten sind im gemeinsamen Stil nutzbar; spätere Ausbaustufen folgen |
|
| M7 | Landingpage | Public-Seite und Auth-Seiten sind im gemeinsamen Stil nutzbar; spätere Ausbaustufen folgen |
|
||||||
| M8 | Härtung | Betrieb, Datenschutz, Monitoring und Isolation sind geprüft |
|
| M8 | Härtung | Betrieb, Datenschutz, Monitoring und Isolation sind geprüft |
|
||||||
@@ -515,9 +515,15 @@ Schritte:
|
|||||||
`index.php`. Ein eigener `/app/einzahlungen`-Screen wie ursprünglich in der
|
`index.php`. Ein eigener `/app/einzahlungen`-Screen wie ursprünglich in der
|
||||||
Zielarchitektur skizziert existiert nicht separat; die Funktion ist bewusst
|
Zielarchitektur skizziert existiert nicht separat; die Funktion ist bewusst
|
||||||
im Dashboard gebündelt statt als eigene Route.
|
im Dashboard gebündelt statt als eigene Route.
|
||||||
- Mitgliederverwaltung tenant- und rollenbasiert umsetzen: erledigt in
|
- Mitgliederverwaltung tenant- und rollenbasiert umsetzen: erledigt.
|
||||||
`mitarbeiterverwalten.php`, inklusive Spiegelung nach `participants` und
|
`mitarbeiterverwalten.php` verwaltet jetzt `participants` als primäre,
|
||||||
Behebung einer gespeicherten XSS-Lücke bei Name/E-Mail.
|
tenant-scoped Quelle (nicht mehr die global unscoped `kl_Mitarbeiter`-
|
||||||
|
Tabelle) und erlaubt Admins, Mitgliedern unabhängig von Name/E-Mail einen
|
||||||
|
Login-Zugang mit Rolle zu gewähren oder zu entziehen (Einladung per Mail
|
||||||
|
über den bestehenden Passwort-Reset-Mechanismus, Entzug als Statuswechsel
|
||||||
|
statt Delete). Nebenbei behoben: eine gespeicherte XSS-Lücke bei Name/
|
||||||
|
E-Mail und ein Mandanten-Datenleck, weil die alte Version jedem
|
||||||
|
SaaS-Mandanten die komplette Default-Mandanten-Mitgliederliste zeigte.
|
||||||
- Gesamtübersicht umsetzen: erster read-only Stand in `kaffeeliste.php`
|
- Gesamtübersicht umsetzen: erster read-only Stand in `kaffeeliste.php`
|
||||||
erledigt.
|
erledigt.
|
||||||
- Teilnehmerauswertung umsetzen: read-only Stand in `teilnehmerauswertung.php`
|
- Teilnehmerauswertung umsetzen: read-only Stand in `teilnehmerauswertung.php`
|
||||||
@@ -529,8 +535,10 @@ Schritte:
|
|||||||
- Sammelerfassung (`stricheintragen.php`, `einzahlung.php`) tenant-sicher
|
- Sammelerfassung (`stricheintragen.php`, `einzahlung.php`) tenant-sicher
|
||||||
absichern und ans Ledger anbinden: erledigt. Beide Seiten hatten zuvor
|
absichern und ans Ledger anbinden: erledigt. Beide Seiten hatten zuvor
|
||||||
keine Zugriffskontrolle außer CSRF; das ist behoben.
|
keine Zugriffskontrolle außer CSRF; das ist behoben.
|
||||||
- Hinweise als tenant-spezifische Notices umsetzen: offen, `hinweise.php`
|
- Hinweise als tenant-spezifische Notices umsetzen: erledigt. Neue Tabelle
|
||||||
ist noch vollständig Legacy ohne Tenant-Bezug.
|
`notices` mit Soft-Delete, `hinweise.php` und die Banner-Anzeige in
|
||||||
|
`header.php` sind tenant-scoped umgestellt; `kl_hinweise` bleibt nur noch
|
||||||
|
als Golden-Master-Referenz bestehen.
|
||||||
|
|
||||||
Ergebnis:
|
Ergebnis:
|
||||||
|
|
||||||
@@ -717,7 +725,9 @@ UX-Prüfungen:
|
|||||||
werden?
|
werden?
|
||||||
- Wie sollen Kunden-Tenants adressiert werden: Subdomain, Pfad oder Auswahl nach
|
- Wie sollen Kunden-Tenants adressiert werden: Subdomain, Pfad oder Auswahl nach
|
||||||
Login?
|
Login?
|
||||||
- Welche Rolle soll `treasurer` haben: eigene Rolle oder Teil von `admin`?
|
- ~~Welche Rolle soll `treasurer` haben: eigene Rolle oder Teil von `admin`?~~
|
||||||
|
Entschieden mit der M5-Zugangsvergabe: `treasurer` ist eine eigene,
|
||||||
|
über `mitarbeiterverwalten.php` vergebbare Rolle, getrennt von `admin`.
|
||||||
- Wird das Umfrage-Modul Teil des SaaS-Produkts oder nur archiviert?
|
- Wird das Umfrage-Modul Teil des SaaS-Produkts oder nur archiviert?
|
||||||
- Braucht der MVP schon Tarife/Billing oder erstmal nur Registrierung?
|
- Braucht der MVP schon Tarife/Billing oder erstmal nur Registrierung?
|
||||||
- Sollen bestehende Kunden per Einladung oder per Self-Service onboarden?
|
- Sollen bestehende Kunden per Einladung oder per Self-Service onboarden?
|
||||||
|
|||||||
+20
-8
@@ -14,20 +14,32 @@
|
|||||||
<body class="is-preload">
|
<body class="is-preload">
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
//echo "<div style='background-color: #ffeb3b; padding: 10px; text-align: center; font-weight: bold;'>1</div>";
|
require_once __DIR__ . '/app/ledger.php';
|
||||||
// Aktuelle Hinweise abrufen
|
require_once __DIR__ . '/app/notices.php';
|
||||||
$sql = "SELECT nachricht FROM kl_hinweise WHERE gueltig_bis >= SYSDATETIME() ORDER BY gueltig_bis ASC";
|
|
||||||
$stmt = sqlsrv_query($conn, $sql);
|
|
||||||
|
|
||||||
if ($stmt === false) {
|
// Aktuellen Hinweis fuer den passenden Mandanten abrufen: eingeloggte
|
||||||
die(print_r(sqlsrv_errors(), true));
|
// SaaS-Nutzer sehen den Hinweis ihres Mandanten, der Legacy-/Dev-Fallback
|
||||||
|
// zeigt den Hinweis des Default-Mandanten.
|
||||||
|
$headerPdo = app_db_pdo();
|
||||||
|
$headerSaasUser = saas_current_user($headerPdo);
|
||||||
|
$headerNoticeTenantId = 0;
|
||||||
|
if ($headerSaasUser !== null) {
|
||||||
|
$headerNoticeTenantId = (int)$headerSaasUser['tenant_id'];
|
||||||
|
} else {
|
||||||
|
$headerDefaultTenant = ledger_fetch_default_tenant($headerPdo);
|
||||||
|
if ($headerDefaultTenant !== null) {
|
||||||
|
$headerNoticeTenantId = (int)$headerDefaultTenant['id'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
|
if ($headerNoticeTenantId > 0) {
|
||||||
|
$headerNotice = notices_fetch_active($headerPdo, $headerNoticeTenantId);
|
||||||
|
if ($headerNotice !== null) {
|
||||||
echo "<div style='background-color: #ffeb3b; padding: 25px; text-align: center; font-weight: bold; font-size: 20px;'>"
|
echo "<div style='background-color: #ffeb3b; padding: 25px; text-align: center; font-weight: bold; font-size: 20px;'>"
|
||||||
. htmlspecialchars($row['nachricht']) .
|
. saas_html($headerNotice['message']) .
|
||||||
"</div>";
|
"</div>";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<!-- Wrapper -->
|
<!-- Wrapper -->
|
||||||
|
|||||||
+31
-25
@@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
include "functions.php";
|
include "functions.php";
|
||||||
|
require_once __DIR__ . "/app/ledger.php";
|
||||||
|
require_once __DIR__ . "/app/notices.php";
|
||||||
app_require_csrf();
|
app_require_csrf();
|
||||||
include "header.php";
|
include "header.php";
|
||||||
include "headerline.php";
|
include "headerline.php";
|
||||||
@@ -17,47 +19,51 @@ include "nav.php";
|
|||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
if(checkKaffeelisteAdmin($conn, $mailadress)){
|
$pdo = app_db_pdo();
|
||||||
|
$saasUser = saas_current_user($pdo);
|
||||||
|
$tenantId = 0;
|
||||||
|
$hasAccess = false;
|
||||||
|
|
||||||
|
if ($saasUser !== null && saas_user_has_role(['owner', 'admin'], $saasUser)) {
|
||||||
|
$tenantId = (int)$saasUser['tenant_id'];
|
||||||
|
$hasAccess = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$hasAccess && $saasUser === null && checkKaffeelisteAdmin($conn, $mailadress)) {
|
||||||
|
$tenant = ledger_fetch_default_tenant($pdo);
|
||||||
|
if ($tenant !== null) {
|
||||||
|
$tenantId = (int)$tenant['id'];
|
||||||
|
$hasAccess = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($hasAccess){
|
||||||
|
|
||||||
echo "<h2>Kaffeeliste - Hinweise</h2>";
|
echo "<h2>Kaffeeliste - Hinweise</h2>";
|
||||||
|
|
||||||
// Hinweis speichern oder löschen
|
// Hinweis speichern oder als geloescht markieren
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$aktion = $_POST['aktion'] ?? 'speichern';
|
$aktion = $_POST['aktion'] ?? 'speichern';
|
||||||
|
|
||||||
if ($aktion === 'loeschen') {
|
if ($aktion === 'loeschen') {
|
||||||
$id = (int)($_POST['id'] ?? 0);
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
if ($id > 0) {
|
if ($id > 0) {
|
||||||
$stmt = sqlsrv_query($conn, "DELETE FROM kl_hinweise WHERE id = ?", [$id]);
|
notices_soft_delete($pdo, $tenantId, $id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$nachricht = $_POST['nachricht'];
|
$nachricht = $_POST['nachricht'] ?? '';
|
||||||
$gueltig_bis = $_POST['gueltig_bis']; // z.B. "2025-09-03T14:00"
|
$gueltig_bis = $_POST['gueltig_bis'] ?? ''; // z.B. "2025-09-03T14:00"
|
||||||
$dt = DateTime::createFromFormat('Y-m-d\TH:i', $gueltig_bis);
|
$dt = DateTime::createFromFormat('Y-m-d\TH:i', $gueltig_bis);
|
||||||
|
|
||||||
if ($dt) {
|
if ($dt) {
|
||||||
$gueltig_bis_sql = $dt->format('Y-m-d H:i:s'); // z.B. "2025-09-03 14:00:00"
|
$gueltig_bis_sql = $dt->format('Y-m-d H:i:s'); // z.B. "2025-09-03 14:00:00"
|
||||||
} else {
|
$createdByUserId = $saasUser !== null ? (int)$saasUser['user_id'] : null;
|
||||||
die("Ungültiges Datumsformat");
|
notices_create($pdo, $tenantId, (string)$nachricht, $gueltig_bis_sql, $createdByUserId);
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($nachricht) && !empty($gueltig_bis_sql)) {
|
|
||||||
|
|
||||||
$stmt = sqlsrv_query($conn,
|
|
||||||
"INSERT INTO kl_hinweise (nachricht, gueltig_bis) VALUES (?, ?)",
|
|
||||||
[$nachricht, $gueltig_bis_sql]
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hinweise abrufen
|
$hinweise = notices_fetch_all($pdo, $tenantId);
|
||||||
$hinweise = [];
|
|
||||||
$stmt = sqlsrv_query($conn, "SELECT id, nachricht, gueltig_bis FROM kl_hinweise ORDER BY gueltig_bis DESC");
|
|
||||||
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
|
|
||||||
$hinweise[] = $row;
|
|
||||||
}
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
|
||||||
@@ -75,11 +81,11 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
|
|||||||
<h2>Alle Hinweise</h2>
|
<h2>Alle Hinweise</h2>
|
||||||
<?php foreach ($hinweise as $hinweis): ?>
|
<?php foreach ($hinweise as $hinweis): ?>
|
||||||
<div class="hinweis">
|
<div class="hinweis">
|
||||||
<strong><?= htmlspecialchars($hinweis['nachricht']) ?></strong><br>
|
<strong><?php echo saas_html($hinweis['message']); ?></strong><br>
|
||||||
<small>Gültig bis: <?= $hinweis['gueltig_bis']->format('d.m.Y H:i') ?></small><br>
|
<small>Gültig bis: <?php echo saas_html((new DateTimeImmutable($hinweis['valid_until']))->format('d.m.Y H:i')); ?></small><br>
|
||||||
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" onsubmit="return confirm('Diesen Hinweis wirklich löschen?')">
|
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" onsubmit="return confirm('Diesen Hinweis wirklich löschen?')">
|
||||||
<input type="hidden" name="aktion" value="loeschen">
|
<input type="hidden" name="aktion" value="loeschen">
|
||||||
<input type="hidden" name="id" value="<?= (int)$hinweis['id'] ?>">
|
<input type="hidden" name="id" value="<?php echo (int)$hinweis['id']; ?>">
|
||||||
<?php echo app_csrf_field(); ?>
|
<?php echo app_csrf_field(); ?>
|
||||||
<button type="submit">Löschen</button>
|
<button type="submit">Löschen</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+198
-192
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
include "functions.php";
|
include "functions.php";
|
||||||
require_once __DIR__ . "/app/ledger.php";
|
require_once __DIR__ . "/app/ledger.php";
|
||||||
|
require_once __DIR__ . "/app/saas-mail.php";
|
||||||
app_require_csrf();
|
app_require_csrf();
|
||||||
include "header.php";
|
include "header.php";
|
||||||
include "headerline.php";
|
include "headerline.php";
|
||||||
@@ -38,134 +39,96 @@ if (!$hasAccess && $saasUser === null && checkKaffeelisteAdmin($conn, $mailadres
|
|||||||
|
|
||||||
if($hasAccess){
|
if($hasAccess){
|
||||||
|
|
||||||
|
$meldung = null;
|
||||||
|
$fehler = null;
|
||||||
|
$einladungslink = null;
|
||||||
|
$bearbeitenId = null;
|
||||||
|
|
||||||
// Funktion zum Anlegen, Bearbeiten und Deaktivieren von Mitgliedern. Der
|
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||||
// gespiegelte participants-Datensatz wird in derselben Transaktion
|
$aktion = $_POST["aktion"] ?? '';
|
||||||
// nachgezogen, damit Ledger-Ansichten sofort den neuen Stand zeigen.
|
|
||||||
function bearbeiteMitglied($aktion, $mitgliedID, $name, $email, $paypalname, $aktiv, $admin, PDO $pdo, int $tenantId) {
|
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.';
|
||||||
|
} else {
|
||||||
try {
|
try {
|
||||||
if ($aktion === 'anlegen') {
|
ledger_create_participant($pdo, $tenantId, $name, $email, $paypalname, $aktiv);
|
||||||
$sql = "INSERT INTO kl_Mitarbeiter (Name, Email, paypalname, aktiv, admin) VALUES (?, ?, ?, ?, ?)";
|
$meldung = 'Mitglied wurde angelegt.';
|
||||||
$params = array($name, $email, $paypalname, $aktiv, $admin);
|
|
||||||
} elseif ($aktion === 'bearbeitenspeichern') {
|
|
||||||
$sql = "UPDATE kl_Mitarbeiter SET Name = ?, Email = ?, paypalname = ?, aktiv = ?, admin = ? WHERE MitarbeiterID = ?";
|
|
||||||
$params = array($name, $email, $paypalname, $aktiv, $admin, $mitgliedID);
|
|
||||||
} elseif ($aktion === 'aktivieren') {
|
|
||||||
$sql = "UPDATE kl_Mitarbeiter SET aktiv = 1 WHERE MitarbeiterID = ?";
|
|
||||||
$params = array($mitgliedID);
|
|
||||||
} elseif ($aktion === 'deaktivieren') {
|
|
||||||
$sql = "UPDATE kl_Mitarbeiter SET aktiv = 0 WHERE MitarbeiterID = ?";
|
|
||||||
$params = array($mitgliedID);
|
|
||||||
} else {
|
|
||||||
throw new Exception("Ungültige Aktion.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
|
|
||||||
$legacyMitarbeiterId = $aktion === 'anlegen' ? (int)$pdo->lastInsertId() : (int)$mitgliedID;
|
|
||||||
ledger_mirror_legacy_participant($pdo, $tenantId, $legacyMitarbeiterId);
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
|
|
||||||
return true; // Erfolgreich
|
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
if ($pdo->inTransaction()) {
|
$fehler = 'Das Mitglied konnte nicht angelegt werden (E-Mail eventuell schon vergeben).';
|
||||||
$pdo->rollBack();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Die Aktion konnte nicht gespeichert werden.';
|
|
||||||
}
|
}
|
||||||
}?>
|
} 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"]);
|
||||||
|
|
||||||
<!-- Formular für das Bearbeiten von Mitgliedern -->
|
if ($name === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
<?php
|
$fehler = 'Bitte einen Namen und eine gültige E-Mail-Adresse angeben.';
|
||||||
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["aktion"]) && $_POST["aktion"] === 'bearbeiten') {
|
} elseif (ledger_update_participant($pdo, $tenantId, $participantId, $name, $email, $paypalname, $aktiv)) {
|
||||||
$mitgliedID = (int)$_POST["mitgliedID"];
|
$meldung = 'Mitglied wurde gespeichert.';
|
||||||
|
|
||||||
// Informationen des ausgewählten Mitglieds abrufen
|
|
||||||
$stmtEinzelmitglied = $pdo->prepare("SELECT * FROM kl_Mitarbeiter WHERE MitarbeiterID = ?");
|
|
||||||
$stmtEinzelmitglied->execute([$mitgliedID]);
|
|
||||||
$einzelmitglied = $stmtEinzelmitglied->fetch();
|
|
||||||
?>
|
|
||||||
<h3>Bearbeiten von <?php echo saas_html($einzelmitglied['Name']); ?></h3>
|
|
||||||
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
|
|
||||||
<input type="hidden" name="aktion" value="bearbeitenspeichern">
|
|
||||||
<input type="hidden" name="mitgliedID" value="<?php echo $mitgliedID; ?>">
|
|
||||||
<?php echo app_csrf_field(); ?>
|
|
||||||
|
|
||||||
<label for="name">Name:</label>
|
|
||||||
<input type="text" name="name" id="name" value="<?php echo saas_html($einzelmitglied['Name']); ?>" required>
|
|
||||||
|
|
||||||
<label for="email">E-Mail:</label>
|
|
||||||
<input type="email" name="email" id="email" value="<?php echo saas_html($einzelmitglied['Email']); ?>" required>
|
|
||||||
|
|
||||||
<label for="paypalname">PayPal-Name:</label>
|
|
||||||
<input type="text" name="paypalname" id="paypalname" value="<?php echo htmlspecialchars($einzelmitglied['paypalname'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
|
||||||
<br>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" name="aktiv" id="aktiv" <?php echo $einzelmitglied['aktiv'] ? 'checked' : ''; ?>>
|
|
||||||
<label class="form-check-label" for="aktiv">Aktiv:</label>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" name="admin" id="admin" <?php echo $einzelmitglied['admin'] ? 'checked' : ''; ?>>
|
|
||||||
<label class="form-check-label" for="admin">Administrator:</label>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<button type="submit">Speichern</button>
|
|
||||||
</form>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verarbeitung des Formulars, wenn es gesendet wurde
|
|
||||||
elseif ($_SERVER["REQUEST_METHOD"] == "POST") {
|
|
||||||
$aktion = $_POST["aktion"];
|
|
||||||
|
|
||||||
if ($aktion === 'anlegen' || $aktion === 'bearbeitenspeichern') {
|
|
||||||
$mitgliedID = isset($_POST["mitgliedID"]) ? (int)$_POST["mitgliedID"] : null;
|
|
||||||
$name = $_POST["name"];
|
|
||||||
$email = $_POST["email"];
|
|
||||||
$paypalname = trim($_POST["paypalname"] ?? '');
|
|
||||||
$paypalname = $paypalname !== '' ? $paypalname : null;
|
|
||||||
$aktiv = isset($_POST["aktiv"]) ? 1 : 0;
|
|
||||||
$admin = isset($_POST["admin"]) ? 1 : 0;
|
|
||||||
|
|
||||||
$ergebnis = bearbeiteMitglied($aktion, $mitgliedID, $name, $email, $paypalname, $aktiv, $admin, $pdo, $tenantId);
|
|
||||||
|
|
||||||
if ($ergebnis === true) {
|
|
||||||
echo "Aktion erfolgreich durchgeführt.";
|
|
||||||
} else {
|
} else {
|
||||||
echo "Fehler: $ergebnis";
|
$fehler = 'Das Mitglied konnte nicht gespeichert werden.';
|
||||||
}
|
}
|
||||||
} elseif ($aktion === 'aktivieren') {
|
} elseif ($aktion === 'aktivieren' || $aktion === 'deaktivieren') {
|
||||||
$mitgliedID = (int)$_POST["mitgliedID"];
|
$participantId = (int)$_POST["mitgliedID"];
|
||||||
|
if (ledger_set_participant_active($pdo, $tenantId, $participantId, $aktion === 'aktivieren')) {
|
||||||
$ergebnis = bearbeiteMitglied('aktivieren', $mitgliedID, null, null, null, null, null, $pdo, $tenantId);
|
$meldung = $aktion === 'aktivieren' ? 'Mitglied wurde aktiviert.' : 'Mitglied wurde deaktiviert.';
|
||||||
|
|
||||||
if ($ergebnis === true) {
|
|
||||||
echo "Mitglied erfolgreich aktiviert.";
|
|
||||||
} else {
|
} else {
|
||||||
echo "Fehler: $ergebnis";
|
$fehler = 'Der Status konnte nicht geändert werden.';
|
||||||
}
|
}
|
||||||
} elseif ($aktion === 'deaktivieren') {
|
} elseif ($aktion === 'zugang_gewaehren') {
|
||||||
$mitgliedID = (int)$_POST["mitgliedID"];
|
$participantId = (int)$_POST["mitgliedID"];
|
||||||
|
$rolle = (string)($_POST["rolle"] ?? '');
|
||||||
|
|
||||||
$ergebnis = bearbeiteMitglied('deaktivieren', $mitgliedID, null, null, null, null, null, $pdo, $tenantId);
|
$ergebnis = saas_grant_participant_access($pdo, $tenantId, $participantId, $rolle);
|
||||||
|
if ($ergebnis['ok']) {
|
||||||
if ($ergebnis === true) {
|
$einladendeTenantSettings = saas_fetch_tenant_settings($pdo, $tenantId);
|
||||||
echo "Mitglied erfolgreich deaktiviert.";
|
$mailErgebnis = saas_send_invite_mail(
|
||||||
} else {
|
$ergebnis['user_email'],
|
||||||
echo "Fehler: $ergebnis";
|
$einladendeTenantSettings['name'] ?? 'Kaffeeliste',
|
||||||
}
|
$rolle,
|
||||||
|
$ergebnis['token']
|
||||||
|
);
|
||||||
|
$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 {
|
} else {
|
||||||
|
$fehler = implode(' ', $ergebnis['errors']);
|
||||||
|
}
|
||||||
|
} elseif ($aktion === 'zugang_entziehen') {
|
||||||
|
$participantId = (int)$_POST["mitgliedID"];
|
||||||
|
|
||||||
|
$ergebnis = saas_revoke_participant_access($pdo, $tenantId, $participantId);
|
||||||
|
if ($ergebnis['ok']) {
|
||||||
|
$meldung = 'Zugang wurde entzogen.';
|
||||||
|
} else {
|
||||||
|
$fehler = implode(' ', $ergebnis['errors']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mitglieder aus der Datenbank abrufen
|
$mitglieder = ledger_fetch_participants_for_admin($pdo, $tenantId);
|
||||||
$stmtMitglieder = $pdo->query("SELECT MitarbeiterID, Name, Email, paypalname, aktiv, admin FROM kl_Mitarbeiter ORDER BY Name");
|
$rollenLabels = ['owner' => 'Inhaber', 'admin' => 'Administrator', 'treasurer' => 'Kassenwart', 'member' => 'Mitglied', 'viewer' => 'Betrachter'];
|
||||||
|
$bearbeitenMitglied = null;
|
||||||
|
if ($bearbeitenId !== null) {
|
||||||
|
foreach ($mitglieder as $m) {
|
||||||
|
if ($m['participant_id'] === $bearbeitenId) {
|
||||||
|
$bearbeitenMitglied = $m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -179,123 +142,166 @@ if($hasAccess){
|
|||||||
|
|
||||||
<h2>Mitglieder verwalten</h2>
|
<h2>Mitglieder verwalten</h2>
|
||||||
|
|
||||||
<!-- Formular für das Anlegen und Bearbeiten von Mitgliedern -->
|
<?php if ($meldung !== null): ?>
|
||||||
<form method="post" action="<?php # echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
|
<div class="hint-box success"><p><?php echo saas_html($meldung); ?></p></div>
|
||||||
<input type="hidden" name="aktion" id="aktion" value="anlegen">
|
<?php endif; ?>
|
||||||
|
<?php if ($fehler !== null): ?>
|
||||||
|
<div class="hint-box error"><p><?php echo saas_html($fehler); ?></p></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($einladungslink !== null): ?>
|
||||||
|
<div class="hint-box"><p>Dev-Link zur Einladung: <a href="<?php echo saas_html($einladungslink); ?>"><?php echo saas_html($einladungslink); ?></a></p></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($bearbeitenMitglied !== null): ?>
|
||||||
|
<h3>Bearbeiten von <?php echo saas_html($bearbeitenMitglied['display_name']); ?></h3>
|
||||||
|
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
|
||||||
|
<input type="hidden" name="aktion" value="bearbeitenspeichern">
|
||||||
|
<input type="hidden" name="mitgliedID" value="<?php echo $bearbeitenMitglied['participant_id']; ?>">
|
||||||
<?php echo app_csrf_field(); ?>
|
<?php echo app_csrf_field(); ?>
|
||||||
|
|
||||||
<label for="name">Name:</label>
|
<label for="name">Name:</label>
|
||||||
<input type="text" name="name" id="name" required>
|
<input type="text" name="name" id="name" value="<?php echo saas_html($bearbeitenMitglied['display_name']); ?>" required>
|
||||||
|
|
||||||
<label for="email">E-Mail:</label>
|
<label for="email">E-Mail:</label>
|
||||||
<input type="email" name="email" id="email" required>
|
<input type="email" name="email" id="email" value="<?php echo saas_html($bearbeitenMitglied['email'] ?? ''); ?>" required>
|
||||||
|
|
||||||
<label for="paypalname">PayPal-Name:</label>
|
<label for="paypalname">PayPal-Name:</label>
|
||||||
<input type="text" name="paypalname" id="paypalname">
|
<input type="text" name="paypalname" id="paypalname" value="<?php echo saas_html($bearbeitenMitglied['paypal_name'] ?? ''); ?>">
|
||||||
<br>
|
<br>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="aktiv" id="aktiv" <?php echo $bearbeitenMitglied['active'] ? 'checked' : ''; ?>>
|
||||||
<input class="form-check-input" type="checkbox" name="aktiv" id="aktiv" checked>
|
<label class="form-check-label" for="aktiv">Aktiv:</label>
|
||||||
<label class="form-check-label" for="aktiv">Aktiv</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<button type="submit">Speichern</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<input class="form-check-input" type="checkbox" name="admin" id="admin" >
|
<!-- Formular für das Anlegen von Mitgliedern -->
|
||||||
<label class="form-check-label" for="admin">Administrator</label>
|
<h3>Neues Mitglied anlegen</h3>
|
||||||
|
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
|
||||||
|
<input type="hidden" name="aktion" id="aktion" value="anlegen">
|
||||||
|
<?php echo app_csrf_field(); ?>
|
||||||
|
|
||||||
|
<label for="new-name">Name:</label>
|
||||||
|
<input type="text" name="name" id="new-name" required>
|
||||||
|
|
||||||
|
<label for="new-email">E-Mail:</label>
|
||||||
|
<input type="email" name="email" id="new-email" required>
|
||||||
|
|
||||||
|
<label for="new-paypalname">PayPal-Name:</label>
|
||||||
|
<input type="text" name="paypalname" id="new-paypalname">
|
||||||
|
<br>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="aktiv" id="new-aktiv" checked>
|
||||||
|
<label class="form-check-label" for="new-aktiv">Aktiv</label>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit">Mitglied anlegen</button>
|
<button type="submit">Mitglied anlegen</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
<!-- Tabelle zur Anzeige und Bearbeitung von Mitgliedern -->
|
<!-- Tabelle zur Anzeige und Bearbeitung von Mitgliedern -->
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Mitglied ID</th>
|
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>E-Mail</th>
|
<th>E-Mail</th>
|
||||||
<th>PayPal-Name</th>
|
<th>PayPal-Name</th>
|
||||||
<th>Aktiv</th>
|
<th>Aktiv</th>
|
||||||
<th>Administrator</th>
|
<th>Zugang</th>
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
<?php
|
<?php foreach ($mitglieder as $mitglied): ?>
|
||||||
|
<tr>
|
||||||
while ($row = $stmtMitglieder->fetch()) {
|
<td><?php echo saas_html($mitglied['display_name']); ?></td>
|
||||||
$mitgliedIdRow = (int)$row['MitarbeiterID'];
|
<td><?php echo saas_html($mitglied['email'] ?? ''); ?></td>
|
||||||
|
<td><?php echo saas_html($mitglied['paypal_name'] ?? ''); ?></td>
|
||||||
echo '<tr >';
|
<td><?php echo $mitglied['active'] ? '1' : '0'; ?></td>
|
||||||
echo "<td>{$mitgliedIdRow}</td>";
|
<td>
|
||||||
echo "<td>" . saas_html($row['Name']) . "</td>";
|
<?php if ($mitglied['membership_status'] === 'active'): ?>
|
||||||
echo "<td>" . saas_html($row['Email']) . "</td>";
|
<?php if ($mitglied['role'] === 'owner'): ?>
|
||||||
echo "<td>" . saas_html($row['paypalname'] ?? '') . "</td>";
|
Inhaber
|
||||||
echo "<td>{$row['aktiv']}</td>";
|
<?php else: ?>
|
||||||
echo "<td>{$row['admin']}</td>";
|
<?php echo saas_html($rollenLabels[$mitglied['role']] ?? $mitglied['role']); ?>
|
||||||
echo "<td>";
|
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" style="display:inline">
|
||||||
echo '<ul class="actions">
|
<input type="hidden" name="aktion" value="zugang_gewaehren">
|
||||||
<li>';
|
<input type="hidden" name="mitgliedID" value="<?php echo $mitglied['participant_id']; ?>">
|
||||||
|
<?php echo app_csrf_field(); ?>
|
||||||
echo "<form method='post' action='{$_SERVER["PHP_SELF"]}'>";
|
<select name="rolle">
|
||||||
echo "<input type='hidden' name='aktion' value='bearbeiten'>";
|
<?php foreach (saas_grantable_roles() as $rolle): ?>
|
||||||
echo "<input type='hidden' name='mitgliedID' value='{$row['MitarbeiterID']}'>";
|
<option value="<?php echo saas_html($rolle); ?>" <?php echo $rolle === $mitglied['role'] ? 'selected' : ''; ?>><?php echo saas_html($rollenLabels[$rolle] ?? $rolle); ?></option>
|
||||||
echo app_csrf_field();
|
<?php endforeach; ?>
|
||||||
echo "<button type='submit'>Bearbeiten</button>";
|
</select>
|
||||||
echo "</form></li><li>";
|
<button type="submit">Rolle ändern</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" style="display:inline">
|
||||||
if ($row['aktiv'] == 1) {
|
<input type="hidden" name="aktion" value="zugang_entziehen">
|
||||||
echo "<form method='post' action='{$_SERVER["PHP_SELF"]}'>";
|
<input type="hidden" name="mitgliedID" value="<?php echo $mitglied['participant_id']; ?>">
|
||||||
echo "<input type='hidden' name='aktion' value='deaktivieren'>";
|
<?php echo app_csrf_field(); ?>
|
||||||
echo "<input type='hidden' name='mitgliedID' value='{$row['MitarbeiterID']}'>";
|
<button type="submit">Zugang entziehen</button>
|
||||||
echo app_csrf_field();
|
</form>
|
||||||
echo "<button type='submit'>Deaktivieren</button>";
|
<?php endif; ?>
|
||||||
echo "</form>";
|
<?php else: ?>
|
||||||
} else {
|
<?php echo $mitglied['membership_status'] === 'revoked' ? 'Zugang entzogen' : 'Kein Zugang'; ?>
|
||||||
echo "<form method='post' action='{$_SERVER["PHP_SELF"]}'>";
|
<?php if ($mitglied['email'] !== null && $mitglied['email'] !== ''): ?>
|
||||||
echo "<input type='hidden' name='aktion' value='aktivieren'>";
|
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" style="display:inline">
|
||||||
echo "<input type='hidden' name='mitgliedID' value='{$row['MitarbeiterID']}'>";
|
<input type="hidden" name="aktion" value="zugang_gewaehren">
|
||||||
echo app_csrf_field();
|
<input type="hidden" name="mitgliedID" value="<?php echo $mitglied['participant_id']; ?>">
|
||||||
echo "<button type='submit'>Aktivieren</button>";
|
<?php echo app_csrf_field(); ?>
|
||||||
echo "</form>";
|
<select name="rolle">
|
||||||
}
|
<?php foreach (saas_grantable_roles() as $rolle): ?>
|
||||||
echo "</li></ul></td>";
|
<option value="<?php echo saas_html($rolle); ?>"><?php echo saas_html($rollenLabels[$rolle] ?? $rolle); ?></option>
|
||||||
echo "</tr>";
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
}
|
<button type="submit">Zugang gewähren</button>
|
||||||
?>
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<ul class="actions">
|
||||||
|
<li>
|
||||||
|
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
|
||||||
|
<input type="hidden" name="aktion" value="bearbeiten">
|
||||||
|
<input type="hidden" name="mitgliedID" value="<?php echo $mitglied['participant_id']; ?>">
|
||||||
|
<?php echo app_csrf_field(); ?>
|
||||||
|
<button type="submit">Bearbeiten</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<?php if ($mitglied['active']): ?>
|
||||||
|
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
|
||||||
|
<input type="hidden" name="aktion" value="deaktivieren">
|
||||||
|
<input type="hidden" name="mitgliedID" value="<?php echo $mitglied['participant_id']; ?>">
|
||||||
|
<?php echo app_csrf_field(); ?>
|
||||||
|
<button type="submit">Deaktivieren</button>
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
|
||||||
|
<input type="hidden" name="aktion" value="aktivieren">
|
||||||
|
<input type="hidden" name="mitgliedID" value="<?php echo $mitglied['participant_id']; ?>">
|
||||||
|
<?php echo app_csrf_field(); ?>
|
||||||
|
<button type="submit">Aktivieren</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
}else{
|
}else{
|
||||||
echo "<h2>Sie haben keine Zugang zu dieser Webseite</h2>";
|
echo "<h2>Sie haben keine Zugang zu dieser Webseite</h2>";
|
||||||
}
|
}
|
||||||
## Auskommentierung
|
|
||||||
##<link rel="stylesheet" href="/DataTables/datatables.css" />
|
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
<script src="/DataTables/datatables.js"></script>
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
$(document).ready( function () {
|
|
||||||
$('#myTable').DataTable();
|
|
||||||
} );
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<?php include "footer.php"; ?>
|
<?php include "footer.php"; ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user