Anpassung Tenant Admin
This commit is contained in:
@@ -4,9 +4,9 @@ Dieses Repository ist jetzt `SaaS-first` organisiert.
|
||||
|
||||
- `saas-app/` enthaelt die neue Zielanwendung fuer die mandantenfaehige
|
||||
Kaffeeliste.
|
||||
- `docs/` enthaelt Architektur-, Installations- und Migrationsdokumentation.
|
||||
- `docs/` enthaelt Architektur- und Installationsdokumentation.
|
||||
- `legacy-app/` enthaelt den bisherigen PHP-Bestand als archivierte Referenz fuer
|
||||
Fachlogik und Datenmigration.
|
||||
fachliche Orientierung.
|
||||
|
||||
## Produktkern
|
||||
|
||||
@@ -35,7 +35,7 @@ unter `saas-app/public/install/`.
|
||||
Nach dem Setup gibt es jetzt zwei zentrale Web-Einstiege:
|
||||
|
||||
- `saas-app/public/admin/login/` fuer den Global-Admin
|
||||
- `saas-app/public/admin/migration/` fuer die webbasierte Legacy-Migration
|
||||
- `saas-app/public/admin/` fuer die zentrale Verwaltung
|
||||
|
||||
## Hilfsskripte
|
||||
|
||||
@@ -47,8 +47,8 @@ Nach dem Setup gibt es jetzt zwei zentrale Web-Einstiege:
|
||||
|
||||
## Hinweise Zum Umbau
|
||||
|
||||
- `legacy-app/` ist absichtlich nicht geloescht, sondern als Referenz fuer die
|
||||
Daten- und Fachmigration verschoben.
|
||||
- `legacy-app/` ist absichtlich nicht geloescht, sondern als Referenz fuer
|
||||
fachliche Detailfragen verschoben.
|
||||
- Das aktuelle `saas-app/` ist eine konsistente Zielarchitektur mit ueberarbeiteten
|
||||
Views, Modulgrenzen und Betriebsdokumentation.
|
||||
- Ein vollstaendiges Laravel-Bootstrap mit Composer und Runtime bleibt der
|
||||
|
||||
@@ -93,7 +93,7 @@ eigene Umgebung angepasst werden:
|
||||
4. Sicherstellen, dass `open_basedir` nicht nur auf `public/` eingeschraenkt ist. PHP muss mindestens auf den kompletten Ordner `saas-app/` zugreifen duerfen, obwohl der Document-Root auf `public/` zeigt.
|
||||
5. Den Installer unter `/install/` aufrufen und die Einrichtung durchfuehren.
|
||||
6. Danach den Global-Admin unter `/admin/login` anmelden.
|
||||
7. Bei Bedarf die Legacy-Datenmigration unter `/admin/migration` starten.
|
||||
7. In der zentralen Verwaltung unter `/admin/` die ersten Tenants anlegen.
|
||||
8. Nach erfolgreicher Einrichtung den Installer sperren.
|
||||
9. Die Anwendung einmal per Browser aufrufen und die Grundseiten pruefen.
|
||||
|
||||
@@ -124,22 +124,13 @@ Typische Aufgaben:
|
||||
Empfehlung: pro Aufgabe einen klar benannten Cron-Eintrag anlegen und die
|
||||
Ausgabe in Logdateien schreiben.
|
||||
|
||||
## Migration Aus Dem Legacy-System
|
||||
## Zentrale Verwaltung
|
||||
|
||||
Wenn Daten aus der alten Root-Anwendung uebernommen werden sollen, folgt die
|
||||
Reihenfolge:
|
||||
|
||||
1. Mitglieder und Rollen migrieren.
|
||||
2. Einzahlungen und Kaffeebuchungen uebernehmen.
|
||||
3. Hinweise, FAQ und Inhaltsseiten importieren.
|
||||
4. Mandanten-Zuordnung pruefen.
|
||||
5. Danach alte Root-Seiten nur noch lesend oder gar nicht mehr betreiben.
|
||||
|
||||
Der neue Webweg dafuer ist jetzt:
|
||||
Nach der Installation stehen diese Web-Einstiege bereit:
|
||||
|
||||
- `/admin/login` fuer den Global-Admin
|
||||
- `/admin/tenants` fuer die zentrale Tenant-Verwaltung
|
||||
- `/admin/migration` fuer die Legacy-Datenmigration
|
||||
- `/admin/` fuer die zentrale Verwaltungsuebersicht
|
||||
- `/admin/tenants` fuer die Tenant-Verwaltung
|
||||
|
||||
## Betriebscheck
|
||||
|
||||
|
||||
+8
-15
@@ -22,7 +22,7 @@ Kurzfassung:
|
||||
1. Webserver auf `public/` ausrichten.
|
||||
2. Im Browser `https://deine-domain.tld/install/` aufrufen.
|
||||
3. `.env`, DB-Zugang und Tenancy-Werte ueber den Installer speichern.
|
||||
4. SQL-Bundle erzeugen und wenn moeglich Migrationen direkt ausfuehren.
|
||||
4. SQL-Bundle erzeugen und wenn moeglich das Datenbankschema direkt ausfuehren.
|
||||
5. Installer sperren.
|
||||
6. Einen ersten Mandanten und erste Benutzer anlegen.
|
||||
7. Cron-Jobs fuer Queue, Import, Export und Benachrichtigungen einrichten.
|
||||
@@ -45,19 +45,13 @@ Skripte verarbeitet:
|
||||
Wenn `pdo_mysql` lokal oder auf dem Hosting nicht verfuegbar ist, muss das
|
||||
erzeugte SQL-Bundle manuell gegen MySQL/MariaDB ausgefuehrt werden.
|
||||
|
||||
## Migration Aus Dem Legacy-System
|
||||
## Zentrale Verwaltung
|
||||
|
||||
Die fachliche Roadmap und der Uebergang aus dem alten Root-System sind in
|
||||
`../docs/implementation-foundation.md` beschrieben.
|
||||
Nach der Installation erfolgt die zentrale Administration ueber:
|
||||
|
||||
Der relevante Kern der alten Anwendung besteht im Wesentlichen aus:
|
||||
|
||||
- Dashboard und Kontostand
|
||||
- Mitgliederverwaltung
|
||||
- Kaffee-Striche
|
||||
- Einzahlungen
|
||||
- Hinweise und Inhalte
|
||||
- Exporte und operative Hilfsfunktionen
|
||||
- `public/admin/login/`
|
||||
- `public/admin/`
|
||||
- `public/admin/tenants/`
|
||||
|
||||
## Hosting-Hinweise
|
||||
|
||||
@@ -70,6 +64,5 @@ Der relevante Kern der alten Anwendung besteht im Wesentlichen aus:
|
||||
|
||||
## Aktueller Stand
|
||||
|
||||
Das Verzeichnis ist als Zielarchitektur vorbereitet. Es ersetzt den Legacy-Root
|
||||
noch nicht vollstaendig, sondern dient als naechster konsistenter Zielzustand
|
||||
fuer die SaaS-Umstellung.
|
||||
Das Verzeichnis ist als Zielarchitektur fuer den Neuaufbau vorbereitet und
|
||||
dient als konsistenter Zielzustand fuer die SaaS-Umstellung.
|
||||
|
||||
+114
-164
@@ -22,13 +22,39 @@ function admin_badge(string $label, string $tone = 'neutral'): string
|
||||
return '<span class="badge badge-' . admin_h($tone) . '">' . admin_h($label) . '</span>';
|
||||
}
|
||||
|
||||
$path = parse_url((string) ($_SERVER['REQUEST_URI'] ?? '/admin/login'), PHP_URL_PATH);
|
||||
function admin_summary_metrics(array $tenants): array
|
||||
{
|
||||
$tenantCount = count($tenants);
|
||||
$activeTenants = 0;
|
||||
$memberCount = 0;
|
||||
$adminCount = 0;
|
||||
$providerCount = 0;
|
||||
|
||||
foreach ($tenants as $tenant) {
|
||||
if (($tenant['status'] ?? '') === 'active') {
|
||||
$activeTenants++;
|
||||
}
|
||||
|
||||
$memberCount += (int) ($tenant['member_count'] ?? 0);
|
||||
$adminCount += (int) ($tenant['admin_count'] ?? 0);
|
||||
$providerCount += (int) ($tenant['provider_count'] ?? 0);
|
||||
}
|
||||
|
||||
return [
|
||||
['label' => 'Mandanten gesamt', 'value' => (string) $tenantCount, 'detail' => 'Mandanten im zentralen Portfolio.'],
|
||||
['label' => 'Aktive Mandanten', 'value' => (string) $activeTenants, 'detail' => 'Bereiche mit aktivem Status.'],
|
||||
['label' => 'Mitglieder gesamt', 'value' => (string) $memberCount, 'detail' => 'Aktive Zuordnungen über alle Mandanten.'],
|
||||
['label' => 'SSO-Provider', 'value' => (string) $providerCount, 'detail' => 'Aktive Identitätsanbieter für die Anmeldung.'],
|
||||
['label' => 'Tenant-Admins', 'value' => (string) $adminCount, 'detail' => 'Verwaltungszugänge in den Mandanten.'],
|
||||
];
|
||||
}
|
||||
|
||||
$path = parse_url((string) ($_SERVER['REQUEST_URI'] ?? '/admin/login/'), PHP_URL_PATH);
|
||||
$path = is_string($path) ? rtrim($path, '/') : '/admin/login';
|
||||
$page = match ($path) {
|
||||
'/admin', '/admin/' => 'tenants',
|
||||
'/admin', '/admin/' => 'overview',
|
||||
'/admin/login' => 'login',
|
||||
'/admin/tenants' => 'tenants',
|
||||
'/admin/migration' => 'migration',
|
||||
'/admin/logout' => 'logout',
|
||||
default => 'login',
|
||||
};
|
||||
@@ -40,27 +66,15 @@ $pdo = null;
|
||||
$adminLogin = ['state' => app_admin_login_state(), 'message' => null, 'error' => null];
|
||||
$tenants = [];
|
||||
$editingTenant = null;
|
||||
$migrationResult = null;
|
||||
$migrationErrors = [];
|
||||
$env = app_env();
|
||||
$migrationValues = [
|
||||
'SOURCE_CONNECTION' => 'sqlsrv',
|
||||
'SOURCE_HOST' => '127.0.0.1',
|
||||
'SOURCE_PORT' => '1433',
|
||||
'SOURCE_DATABASE' => '',
|
||||
'SOURCE_USERNAME' => '',
|
||||
'SOURCE_PASSWORD' => '',
|
||||
'TARGET_TENANT_KEY' => 'legacy-import',
|
||||
'TARGET_TENANT_NAME' => 'Legacy Import',
|
||||
];
|
||||
$summaryMetrics = [];
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && $page === 'logout') {
|
||||
if (hash_equals($_SESSION['admin_csrf'], (string) ($_POST['csrf'] ?? ''))) {
|
||||
app_logout();
|
||||
app_flash('Global-Admin wurde abgemeldet.', 'success');
|
||||
app_flash('Der Global-Admin wurde abgemeldet.', 'success');
|
||||
}
|
||||
|
||||
app_redirect('/admin/login');
|
||||
app_redirect('/admin/login/');
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -74,7 +88,7 @@ if ($page === 'login' && $pdo instanceof PDO) {
|
||||
$admin = app_admin_user();
|
||||
}
|
||||
|
||||
if (in_array($page, ['tenants', 'migration'], true)) {
|
||||
if (in_array($page, ['overview', 'tenants'], true)) {
|
||||
$admin = app_require_platform_admin();
|
||||
|
||||
if (!$pdo instanceof PDO) {
|
||||
@@ -82,60 +96,22 @@ if (in_array($page, ['tenants', 'migration'], true)) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($page === 'tenants' && $pdo instanceof PDO) {
|
||||
if (in_array($page, ['overview', 'tenants'], true) && $pdo instanceof PDO) {
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && !hash_equals($_SESSION['admin_csrf'], (string) ($_POST['csrf'] ?? ''))) {
|
||||
app_flash('CSRF-Status ungueltig. Bitte Seite neu laden.', 'error');
|
||||
app_redirect('/admin/tenants');
|
||||
app_flash('Die Sitzung ist abgelaufen. Bitte lade die Seite neu.', 'error');
|
||||
app_redirect('/admin/tenants/');
|
||||
}
|
||||
|
||||
app_handle_platform_tenant_action($pdo);
|
||||
$tenants = app_admin_tenant_list($pdo);
|
||||
$summaryMetrics = admin_summary_metrics($tenants);
|
||||
|
||||
if (isset($_GET['edit']) && $_GET['edit'] !== '') {
|
||||
$editingTenant = app_query_one($pdo, 'SELECT id, tenant_key, name, status FROM tenants WHERE id = :id LIMIT 1', ['id' => (string) $_GET['edit']]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($page === 'migration') {
|
||||
foreach (array_keys($migrationValues) as $key) {
|
||||
if (isset($_POST[$key])) {
|
||||
$migrationValues[$key] = trim((string) $_POST[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && (string) ($_POST['action'] ?? '') === 'run-legacy-migration') {
|
||||
if (!hash_equals($_SESSION['admin_csrf'], (string) ($_POST['csrf'] ?? ''))) {
|
||||
$migrationErrors[] = 'CSRF-Status ungueltig. Bitte Seite neu laden.';
|
||||
} elseif (!$pdo instanceof PDO) {
|
||||
$migrationErrors[] = $dbError ?? 'Die Zieldatenbank ist aktuell nicht erreichbar.';
|
||||
} else {
|
||||
try {
|
||||
$migrationResult = scripts_migrate_legacy_data(
|
||||
[
|
||||
'connection' => $migrationValues['SOURCE_CONNECTION'],
|
||||
'server' => $migrationValues['SOURCE_HOST'],
|
||||
'port' => $migrationValues['SOURCE_PORT'],
|
||||
'database' => $migrationValues['SOURCE_DATABASE'],
|
||||
'username' => $migrationValues['SOURCE_USERNAME'],
|
||||
'password' => $migrationValues['SOURCE_PASSWORD'],
|
||||
],
|
||||
[
|
||||
'connection' => $env['DB_CONNECTION'] ?? 'mysql',
|
||||
'server' => $env['DB_HOST'] ?? '127.0.0.1',
|
||||
'port' => $env['DB_PORT'] ?? '3306',
|
||||
'database' => $env['DB_DATABASE'] ?? '',
|
||||
'username' => $env['DB_USERNAME'] ?? '',
|
||||
'password' => $env['DB_PASSWORD'] ?? '',
|
||||
],
|
||||
[
|
||||
'tenant_key' => $migrationValues['TARGET_TENANT_KEY'],
|
||||
'tenant_name' => $migrationValues['TARGET_TENANT_NAME'],
|
||||
]
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
$migrationErrors[] = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
if ($page === 'tenants' && isset($_GET['edit']) && $_GET['edit'] !== '') {
|
||||
$editingTenant = app_query_one(
|
||||
$pdo,
|
||||
'SELECT id, tenant_key, name, status FROM tenants WHERE id = :id LIMIT 1',
|
||||
['id' => (string) $_GET['edit']]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +123,7 @@ if ($page === 'migration') {
|
||||
<title>Kaffeeliste Admin</title>
|
||||
<style>
|
||||
:root{--bg:#f4efe7;--card:#fffaf4;--ink:#24160e;--muted:#675546;--brand:#0c6b66;--accent:#a34b12;--line:rgba(36,22,14,.12);--radius:24px;--shadow:0 24px 54px rgba(57,35,22,.11)}
|
||||
*{box-sizing:border-box}body{margin:0;font-family:"Segoe UI",sans-serif;color:var(--ink);background:linear-gradient(180deg,#fbf8f2 0%,var(--bg) 100%)}a{color:inherit}.shell{width:min(1180px,calc(100vw - 32px));margin:24px auto 40px}.bar,.hero,.card,.alert{border:1px solid var(--line);border-radius:var(--radius);background:var(--card);box-shadow:var(--shadow)}.bar,.links,.actions{display:flex;flex-wrap:wrap;gap:10px}.bar{justify-content:space-between;align-items:center;padding:18px 22px;margin-bottom:18px}.hero,.card{padding:24px}.hero{margin-bottom:18px}.grid{display:grid;gap:18px}.grid-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-3{grid-template-columns:repeat(3,minmax(0,1fr))}.links a,.button,button{display:inline-flex;align-items:center;justify-content:center;padding:11px 16px;border-radius:999px;text-decoration:none;font-weight:700;border:0;cursor:pointer}.links a{background:rgba(12,107,102,.08);color:var(--brand)}.links a.active{background:linear-gradient(135deg,var(--brand),#084d49);color:#fff}.button,button{background:linear-gradient(135deg,var(--brand),#084d49);color:#fff}.button.secondary{background:transparent;color:var(--brand);border:1px solid rgba(12,107,102,.18)}h1,h2,h3{margin:0 0 12px;font-family:Georgia,serif}h1{font-size:clamp(2rem,4vw,3.4rem)}h2{font-size:1.35rem}h3{font-size:1.02rem}.eyebrow{display:inline-block;margin-bottom:12px;color:var(--accent);font-size:.82rem;font-weight:800;letter-spacing:.14em;text-transform:uppercase}p,.muted{margin:0;color:var(--muted);line-height:1.65}.stack{display:grid;gap:12px}.metric{padding:18px;border:1px solid var(--line);border-radius:18px;background:#fffdf9}.alert{padding:16px 18px;margin-bottom:18px}.alert-success{background:rgba(17,98,61,.08)}.alert-warning{background:rgba(163,75,18,.08)}.alert-error{background:rgba(154,31,31,.08)}.badge{display:inline-flex;align-items:center;padding:7px 12px;border-radius:999px;font-size:.86rem;font-weight:700}.badge-neutral{background:rgba(12,107,102,.1);color:var(--brand)}.badge-success{background:rgba(17,98,61,.12);color:#11623d}.badge-warning{background:rgba(163,75,18,.12);color:#98510c}.table{overflow-x:auto}.table table{width:100%;border-collapse:collapse;min-width:720px}.table th,.table td{padding:13px 10px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}.table th{font-size:.85rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}form.grid{grid-template-columns:repeat(2,minmax(0,1fr))}label{display:flex;flex-direction:column;gap:8px;font-weight:700}input,select{width:100%;padding:12px 14px;border-radius:16px;border:1px solid rgba(36,22,14,.15);font:inherit;background:#fffdfa;color:var(--ink)}ul{margin:0;padding-left:18px;color:var(--muted);line-height:1.7}.footer{margin-top:18px;text-align:center;color:var(--muted);font-size:.92rem}@media(max-width:960px){.grid-2,.grid-3,form.grid{grid-template-columns:1fr}.bar{align-items:flex-start;flex-direction:column}.table table{min-width:0}}
|
||||
*{box-sizing:border-box}body{margin:0;font-family:"Aptos","Segoe UI",sans-serif;color:var(--ink);background:linear-gradient(180deg,#fbf8f2 0%,var(--bg) 100%)}a{color:inherit}.shell{width:min(1180px,calc(100vw - 32px));margin:24px auto 40px}.bar,.hero,.card,.alert{border:1px solid var(--line);border-radius:var(--radius);background:var(--card);box-shadow:var(--shadow)}.bar,.links,.actions,.context{display:flex;flex-wrap:wrap;gap:10px}.bar{justify-content:space-between;align-items:center;padding:18px 22px;margin-bottom:18px}.hero,.card{padding:24px}.hero{margin-bottom:18px}.grid{display:grid;gap:18px}.grid-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-5{grid-template-columns:repeat(5,minmax(0,1fr))}.links a,.button,button{display:inline-flex;align-items:center;justify-content:center;padding:11px 16px;border-radius:999px;text-decoration:none;font-weight:700;border:0;cursor:pointer}.links a{background:rgba(12,107,102,.08);color:var(--brand)}.links a.active{background:linear-gradient(135deg,var(--brand),#084d49);color:#fff}.button,button{background:linear-gradient(135deg,var(--brand),#084d49);color:#fff}.button.secondary{background:transparent;color:var(--brand);border:1px solid rgba(12,107,102,.18)}h1,h2,h3{margin:0 0 12px;font-family:Georgia,serif}h1{font-size:clamp(2rem,4vw,3.4rem)}h2{font-size:1.35rem}h3{font-size:1.02rem}.eyebrow{display:inline-block;margin-bottom:12px;color:var(--accent);font-size:.82rem;font-weight:800;letter-spacing:.14em;text-transform:uppercase}p,.muted{margin:0;color:var(--muted);line-height:1.65}.stack{display:grid;gap:12px}.metric{padding:18px;border:1px solid var(--line);border-radius:18px;background:#fffdf9}.metric strong{display:block;font-size:1.8rem;margin-bottom:8px}.alert{padding:16px 18px;margin-bottom:18px}.alert-success{background:rgba(17,98,61,.08)}.alert-warning{background:rgba(163,75,18,.08)}.alert-error{background:rgba(154,31,31,.08)}.badge{display:inline-flex;align-items:center;padding:7px 12px;border-radius:999px;font-size:.86rem;font-weight:700}.badge-neutral{background:rgba(12,107,102,.1);color:var(--brand)}.badge-success{background:rgba(17,98,61,.12);color:#11623d}.badge-warning{background:rgba(163,75,18,.12);color:#98510c}.table{overflow-x:auto}.table table{width:100%;border-collapse:collapse;min-width:720px}.table th,.table td{padding:13px 10px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}.table th{font-size:.85rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}form.grid{grid-template-columns:repeat(2,minmax(0,1fr))}label{display:flex;flex-direction:column;gap:8px;font-weight:700}input,select{width:100%;padding:12px 14px;border-radius:16px;border:1px solid rgba(36,22,14,.15);font:inherit;background:#fffdfa;color:var(--ink)}ul{margin:0;padding-left:18px;color:var(--muted);line-height:1.7}.footer{margin-top:18px;text-align:center;color:var(--muted);font-size:.92rem}@media(max-width:960px){.grid-2,.grid-5,form.grid{grid-template-columns:1fr}.bar{align-items:flex-start;flex-direction:column}.table table{min-width:0}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -155,14 +131,14 @@ if ($page === 'migration') {
|
||||
<header class="bar">
|
||||
<div>
|
||||
<strong style="font-family:Georgia,serif;font-size:1.28rem;">Kaffeeliste Admin</strong>
|
||||
<p>Global-Admin Login, Tenant-Verwaltung und Legacy-Migration direkt ueber PHP-Webseiten.</p>
|
||||
<p>Zentrale Verwaltung für Mandanten, Zugänge und Betriebsstatus.</p>
|
||||
</div>
|
||||
<nav class="links">
|
||||
<a href="/admin/login" class="<?= $page === 'login' ? 'active' : '' ?>">Login</a>
|
||||
<a href="/admin/login/" class="<?= $page === 'login' ? 'active' : '' ?>">Login</a>
|
||||
<?php if ($admin !== null): ?>
|
||||
<a href="/admin/tenants" class="<?= $page === 'tenants' ? 'active' : '' ?>">Tenants</a>
|
||||
<a href="/admin/migration" class="<?= $page === 'migration' ? 'active' : '' ?>">Migration</a>
|
||||
<form method="post" action="/admin/logout">
|
||||
<a href="/admin/" class="<?= $page === 'overview' ? 'active' : '' ?>">Übersicht</a>
|
||||
<a href="/admin/tenants/" class="<?= $page === 'tenants' ? 'active' : '' ?>">Mandanten</a>
|
||||
<form method="post" action="/admin/logout/">
|
||||
<input type="hidden" name="csrf" value="<?= admin_h($_SESSION['admin_csrf']) ?>">
|
||||
<button type="submit" class="button secondary">Abmelden</button>
|
||||
</form>
|
||||
@@ -177,64 +153,107 @@ if ($page === 'migration') {
|
||||
<?php if ($page === 'login'): ?>
|
||||
<section class="hero">
|
||||
<div class="eyebrow">Global-Admin</div>
|
||||
<h1>Tenant-Verwaltung nur fuer die zentrale Administration.</h1>
|
||||
<p>Dieser Login ist bewusst vom Mitglieder-Login getrennt und fuehrt direkt in die globale Tenant-Verwaltung.</p>
|
||||
<h1>Zentrale Administration für die Kaffeeliste.</h1>
|
||||
<p>Dieser Zugang ist ausschließlich für die globale Verwaltung gedacht und bleibt bewusst vom Mitglieder-Login getrennt.</p>
|
||||
</section>
|
||||
<?php if ($adminLogin['error'] !== null): ?><section class="alert alert-error"><?= admin_h((string) $adminLogin['error']) ?></section><?php endif; ?>
|
||||
<?php if ($dbError !== null): ?><section class="alert alert-warning"><?= admin_h($dbError) ?></section><?php endif; ?>
|
||||
<section class="grid grid-2">
|
||||
<article class="card">
|
||||
<h2>Anmelden</h2>
|
||||
<form method="post" action="/admin/login" class="stack">
|
||||
<form method="post" action="/admin/login/" class="stack">
|
||||
<input type="hidden" name="action" value="admin-login">
|
||||
<input type="hidden" name="csrf" value="<?= admin_h($_SESSION['admin_csrf']) ?>">
|
||||
<label>E-Mail-Adresse<input type="email" name="email" value="<?= admin_h((string) (($adminLogin['state']['email'] ?? ''))) ?>" autocomplete="email"></label>
|
||||
<label>E-Mail-Adresse<input type="email" name="email" value="<?= admin_h((string) ($adminLogin['state']['email'] ?? '')) ?>" autocomplete="email"></label>
|
||||
<label>Passwort<input type="password" name="password" autocomplete="current-password"></label>
|
||||
<div class="actions"><button type="submit">Als Global-Admin anmelden</button></div>
|
||||
</form>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>Einrichtung</h2>
|
||||
<h2>Hinweise zur Einrichtung</h2>
|
||||
<ul>
|
||||
<li>Der erste Global-Admin wird im Installer angelegt.</li>
|
||||
<li>Die Anmeldung prueft `users.is_platform_admin = 1` und `password_hash`.</li>
|
||||
<li>Nach dem Login stehen Tenant-Verwaltung und Legacy-Migration bereit.</li>
|
||||
<li>Der erste Global-Admin wird im Installer angelegt oder aktualisiert.</li>
|
||||
<li>Die Anmeldung prüft `users.is_platform_admin = 1` und das gespeicherte Passwort.</li>
|
||||
<li>Nach dem Login stehen Übersicht, Mandanten-Verwaltung und Betriebsdaten zentral bereit.</li>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
<?php elseif ($page === 'tenants'): ?>
|
||||
<?php elseif ($page === 'overview'): ?>
|
||||
<section class="hero">
|
||||
<div class="eyebrow">Tenant-Verwaltung</div>
|
||||
<h1>Mandanten zentral anlegen und pflegen</h1>
|
||||
<p>Hier verwaltet der Global-Admin die grundlegenden Tenant-Stammdaten und kommt direkt zur Datenmigration.</p>
|
||||
<div class="eyebrow">Zentrale Übersicht</div>
|
||||
<h1>Alle Mandanten, Zugänge und Kennzahlen an einer Stelle.</h1>
|
||||
<p>Die globale Verwaltung bündelt die wichtigsten Betriebsdaten und führt direkt in die Pflege der Mandanten.</p>
|
||||
</section>
|
||||
<?php if ($dbError !== null): ?><section class="alert alert-warning"><?= admin_h($dbError) ?></section><?php endif; ?>
|
||||
<section class="grid grid-5">
|
||||
<?php foreach ($summaryMetrics as $metric): ?>
|
||||
<article class="metric">
|
||||
<strong><?= admin_h((string) $metric['value']) ?></strong>
|
||||
<h3><?= admin_h((string) $metric['label']) ?></h3>
|
||||
<p><?= admin_h((string) $metric['detail']) ?></p>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</section>
|
||||
<section class="grid grid-2" style="margin-top:18px">
|
||||
<article class="card">
|
||||
<h2>Angemeldet als</h2>
|
||||
<div class="stack">
|
||||
<div class="metric">
|
||||
<h3><?= admin_h((string) ($admin['display_name'] ?? '')) ?></h3>
|
||||
<p><?= admin_h((string) ($admin['email'] ?? '')) ?></p>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<h3>Nächster Schritt</h3>
|
||||
<p>Lege neue Mandanten an, bearbeite bestehende Bereiche und prüfe den Betriebsstatus zentral.</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>Schnellzugriffe</h2>
|
||||
<div class="actions">
|
||||
<a class="button" href="/admin/tenants/">Mandanten verwalten</a>
|
||||
<a class="button secondary" href="/login/">Mitglieder-Login prüfen</a>
|
||||
</div>
|
||||
<ul style="margin-top:16px">
|
||||
<li>Die Mitglieder-Anmeldung bleibt zentral und mandantenfähig.</li>
|
||||
<li>Jeder Mandant behält seine eigenen Mitglieder, Buchungen und Inhalte.</li>
|
||||
<li>Die globale Verwaltung steuert nur die übergreifenden Stammdaten.</li>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<section class="hero">
|
||||
<div class="eyebrow">Mandanten-Verwaltung</div>
|
||||
<h1>Mandanten zentral anlegen und pflegen.</h1>
|
||||
<p>Hier verwaltet der Global-Admin die Stammdaten aller Mandanten und behält Rollen, Mitgliederzahlen und den Login-Betrieb im Blick.</p>
|
||||
</section>
|
||||
<?php if ($dbError !== null): ?><section class="alert alert-warning"><?= admin_h($dbError) ?></section><?php endif; ?>
|
||||
<section class="grid grid-2">
|
||||
<article class="card">
|
||||
<h2><?= $editingTenant !== null ? 'Tenant bearbeiten' : 'Neuen Tenant anlegen' ?></h2>
|
||||
<form method="post" action="/admin/tenants" class="grid">
|
||||
<h2><?= $editingTenant !== null ? 'Mandanten bearbeiten' : 'Neuen Mandanten anlegen' ?></h2>
|
||||
<form method="post" action="/admin/tenants/" class="grid">
|
||||
<input type="hidden" name="csrf" value="<?= admin_h($_SESSION['admin_csrf']) ?>">
|
||||
<input type="hidden" name="action" value="save-tenant">
|
||||
<input type="hidden" name="tenant_id" value="<?= admin_h((string) ($editingTenant['id'] ?? '')) ?>">
|
||||
<label>Tenant-Key<input name="tenant_key" value="<?= admin_h((string) ($editingTenant['tenant_key'] ?? '')) ?>" placeholder="werk-berlin"></label>
|
||||
<label>Mandanten-Key<input name="tenant_key" value="<?= admin_h((string) ($editingTenant['tenant_key'] ?? '')) ?>" placeholder="werk-berlin"></label>
|
||||
<label>Name<input name="name" value="<?= admin_h((string) ($editingTenant['name'] ?? '')) ?>" placeholder="Werk Berlin"></label>
|
||||
<label>Status<select name="status"><?php foreach (['active' => 'Aktiv', 'inactive' => 'Inaktiv', 'sandbox' => 'Sandbox'] as $value => $label): ?><option value="<?= admin_h($value) ?>"<?= (($editingTenant['status'] ?? 'active') === $value) ? ' selected' : '' ?>><?= admin_h($label) ?></option><?php endforeach; ?></select></label>
|
||||
<div class="actions">
|
||||
<button type="submit">Speichern</button>
|
||||
<a class="button secondary" href="/admin/migration">Zur Migration</a>
|
||||
<a class="button secondary" href="/admin/">Zur Übersicht</a>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>Globaler Kontext</h2>
|
||||
<h2>Verwaltungsrahmen</h2>
|
||||
<div class="stack">
|
||||
<div class="metric"><h3>Angemeldet als</h3><p><?= admin_h((string) ($admin['display_name'] ?? '')) ?></p><p class="muted"><?= admin_h((string) ($admin['email'] ?? '')) ?></p></div>
|
||||
<div class="metric"><h3>Naechster Schritt</h3><p>Nach dem Anlegen eines Tenants kann die Legacy-Datenmigration direkt ueber die Weboberflaeche gestartet werden.</p></div>
|
||||
<div class="metric"><h3>Rollenmodell</h3><p>Der Global-Admin pflegt Mandanten zentral. Die operative Arbeit bleibt bei den jeweiligen Tenant-Admins.</p></div>
|
||||
<div class="metric"><h3>Login-Strategie</h3><p>Mitglieder melden sich weiterhin zentral an und werden anschließend automatisch dem passenden Mandanten zugeordnet.</p></div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
<section class="card" style="margin-top:18px">
|
||||
<h2>Vorhandene Tenants</h2>
|
||||
<h2>Vorhandene Mandanten</h2>
|
||||
<div class="table">
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Key</th><th>Status</th><th>Mitglieder</th><th>Admins</th><th>SSO</th><th>Aktion</th></tr></thead>
|
||||
@@ -247,85 +266,16 @@ if ($page === 'migration') {
|
||||
<td><?= admin_h((string) $tenant['member_count']) ?></td>
|
||||
<td><?= admin_h((string) $tenant['admin_count']) ?></td>
|
||||
<td><?= admin_h((string) $tenant['provider_count']) ?></td>
|
||||
<td><a class="button secondary" href="/admin/tenants?edit=<?= admin_h((string) $tenant['id']) ?>">Bearbeiten</a></td>
|
||||
<td><a class="button secondary" href="/admin/tenants/?edit=<?= admin_h((string) $tenant['id']) ?>">Bearbeiten</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<section class="hero">
|
||||
<div class="eyebrow">Legacy-Migration</div>
|
||||
<h1>Alte Kaffeelisten-Daten in das neue Zielformat uebernehmen</h1>
|
||||
<p>Die Seite verbindet sich mit der Quelldatenbank, legt bei Bedarf das neue Schema an und migriert Mitglieder, Striche, Einzahlungen und Hinweise in den ausgewaehlten Tenant.</p>
|
||||
</section>
|
||||
<?php foreach ($migrationErrors as $error): ?><section class="alert alert-error"><?= admin_h($error) ?></section><?php endforeach; ?>
|
||||
<section class="grid grid-2">
|
||||
<article class="card">
|
||||
<h2>Quelle konfigurieren</h2>
|
||||
<form method="post" action="/admin/migration" class="grid">
|
||||
<input type="hidden" name="csrf" value="<?= admin_h($_SESSION['admin_csrf']) ?>">
|
||||
<input type="hidden" name="action" value="run-legacy-migration">
|
||||
<label>Quelle<input name="SOURCE_DATABASE" value="<?= admin_h($migrationValues['SOURCE_DATABASE']) ?>" placeholder="Legacy-Datenbank"></label>
|
||||
<label>Verbindung<select name="SOURCE_CONNECTION"><?php foreach (scripts_supported_connections() as $value => $label): ?><option value="<?= admin_h($value) ?>"<?= $migrationValues['SOURCE_CONNECTION'] === $value ? ' selected' : '' ?>><?= admin_h($label) ?></option><?php endforeach; ?></select></label>
|
||||
<label>Host<input name="SOURCE_HOST" value="<?= admin_h($migrationValues['SOURCE_HOST']) ?>"></label>
|
||||
<label>Port<input name="SOURCE_PORT" value="<?= admin_h($migrationValues['SOURCE_PORT']) ?>"></label>
|
||||
<label>Benutzer<input name="SOURCE_USERNAME" value="<?= admin_h($migrationValues['SOURCE_USERNAME']) ?>"></label>
|
||||
<label>Passwort<input type="password" name="SOURCE_PASSWORD" value="<?= admin_h($migrationValues['SOURCE_PASSWORD']) ?>"></label>
|
||||
<label>Target Tenant-Key<input name="TARGET_TENANT_KEY" value="<?= admin_h($migrationValues['TARGET_TENANT_KEY']) ?>"></label>
|
||||
<label>Target Tenant-Name<input name="TARGET_TENANT_NAME" value="<?= admin_h($migrationValues['TARGET_TENANT_NAME']) ?>"></label>
|
||||
<div class="actions"><button type="submit">Migration starten</button></div>
|
||||
</form>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>Ziel</h2>
|
||||
<ul>
|
||||
<li>Ziel-Datenbank: <code><?= admin_h((string) ($env['DB_CONNECTION'] ?? 'mysql')) ?></code></li>
|
||||
<li>Host: <code><?= admin_h((string) ($env['DB_HOST'] ?? '')) ?></code></li>
|
||||
<li>Datenbank: <code><?= admin_h((string) ($env['DB_DATABASE'] ?? '')) ?></code></li>
|
||||
<li>Die Migration ist idempotent fuer Mitglieder, Tenants und die importierten Kernbuchungen ausgelegt.</li>
|
||||
<li>Fuer SQL Server als Quelle wird auf dem Hosting <code>pdo_sqlsrv</code> benoetigt. Ohne diesen Treiber sollte die Legacy-Datenbank zuerst nach MySQL/MariaDB exportiert werden.</li>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<?php if (is_array($migrationResult)): ?>
|
||||
<section class="grid grid-3" style="margin-top:18px">
|
||||
<?php foreach (($migrationResult['counts'] ?? []) as $label => $value): ?>
|
||||
<article class="metric">
|
||||
<h3><?= admin_h(str_replace('_', ' ', $label)) ?></h3>
|
||||
<p><?= admin_h((string) $value) ?></p>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</section>
|
||||
<section class="grid grid-2" style="margin-top:18px">
|
||||
<article class="card">
|
||||
<h2>Ergebnis</h2>
|
||||
<ul>
|
||||
<li>Schema neu angelegt: <?= admin_h(($migrationResult['schema_created'] ?? false) ? 'ja' : 'nein') ?></li>
|
||||
<li>Target Tenant: <code><?= admin_h((string) ($migrationResult['tenant']['tenant_key'] ?? '')) ?></code> - <?= admin_h((string) ($migrationResult['tenant']['name'] ?? '')) ?></li>
|
||||
</ul>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>Erkannte Legacy-Tabellen</h2>
|
||||
<ul>
|
||||
<?php foreach (($migrationResult['source_tables'] ?? []) as $entry): ?>
|
||||
<li><code><?= admin_h((string) $entry['table']) ?></code>: <?= admin_h($entry['exists'] ? 'vorhanden' : 'fehlt') ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
<?php if (($migrationResult['warnings'] ?? []) !== []): ?>
|
||||
<section class="card" style="margin-top:18px"><h2>Warnungen</h2><ul><?php foreach ($migrationResult['warnings'] as $warning): ?><li><?= admin_h((string) $warning) ?></li><?php endforeach; ?></ul></section>
|
||||
<?php endif; ?>
|
||||
<?php if (($migrationResult['skipped'] ?? []) !== []): ?>
|
||||
<section class="card" style="margin-top:18px"><h2>Nicht automatisch uebernommen</h2><ul><?php foreach ($migrationResult['skipped'] as $warning): ?><li><?= admin_h((string) $warning) ?></li><?php endforeach; ?></ul></section>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="footer">Kaffeeliste Admin | Global-Admin Login, Tenant-Verwaltung und Legacy-Migration ueber PHP-Webseiten</p>
|
||||
<p class="footer">Kaffeeliste Admin | zentrale Mandanten-Verwaltung und globaler Zugriff über PHP-Webseiten</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__, 2) . '/admin.php';
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__, 2) . '/admin.php';
|
||||
header('Location: /admin/', true, 302);
|
||||
exit;
|
||||
|
||||
@@ -64,7 +64,7 @@ function app_pdo(): PDO
|
||||
}
|
||||
|
||||
if (!app_uses_mysql()) {
|
||||
throw new RuntimeException('Diese Public-Runtime ist aktuell fuer MySQL/MariaDB konzipiert.');
|
||||
throw new RuntimeException('Diese Public-Runtime ist aktuell für MySQL/MariaDB ausgelegt.');
|
||||
}
|
||||
|
||||
if (!extension_loaded('pdo_mysql')) {
|
||||
@@ -79,7 +79,7 @@ function app_pdo(): PDO
|
||||
$password = $env['DB_PASSWORD'] ?? '';
|
||||
|
||||
if ($database === '' || $username === '') {
|
||||
throw new RuntimeException('Die Datenbankkonfiguration in saas-app/.env ist unvollstaendig.');
|
||||
throw new RuntimeException('Die Datenbankkonfiguration in `saas-app/.env` ist unvollständig.');
|
||||
}
|
||||
|
||||
$dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $host, $port, $database);
|
||||
@@ -182,9 +182,29 @@ function app_logout(): void
|
||||
unset($_SESSION['auth_user'], $_SESSION['admin_user'], $_SESSION['login_state'], $_SESSION['admin_login_state']);
|
||||
}
|
||||
|
||||
function app_route(string $path): string
|
||||
{
|
||||
$parts = parse_url($path);
|
||||
|
||||
if ($parts === false) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
$routePath = $parts['path'] ?? '/';
|
||||
|
||||
if ($routePath !== '/' && $routePath !== '' && pathinfo($routePath, PATHINFO_EXTENSION) === '') {
|
||||
$routePath = rtrim($routePath, '/') . '/';
|
||||
}
|
||||
|
||||
$query = isset($parts['query']) ? '?' . $parts['query'] : '';
|
||||
$fragment = isset($parts['fragment']) ? '#' . $parts['fragment'] : '';
|
||||
|
||||
return $routePath . $query . $fragment;
|
||||
}
|
||||
|
||||
function app_redirect(string $path): never
|
||||
{
|
||||
header('Location: ' . $path);
|
||||
header('Location: ' . app_route($path));
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -202,8 +222,8 @@ function app_require_auth(): array
|
||||
$auth = app_auth_user();
|
||||
|
||||
if ($auth === null) {
|
||||
app_flash('Bitte zuerst ueber die zentrale Anmeldung einloggen.', 'warning');
|
||||
app_redirect('/login');
|
||||
app_flash('Bitte melde dich zuerst über die zentrale Anmeldung an.', 'warning');
|
||||
app_redirect('/login/');
|
||||
}
|
||||
|
||||
return $auth;
|
||||
@@ -214,8 +234,8 @@ function app_require_platform_admin(): array
|
||||
$admin = app_admin_user();
|
||||
|
||||
if ($admin === null || empty($admin['is_platform_admin'])) {
|
||||
app_flash('Bitte zuerst als Global-Admin einloggen.', 'warning');
|
||||
app_redirect('/admin/login');
|
||||
app_flash('Bitte melde dich zuerst als Global-Admin an.', 'warning');
|
||||
app_redirect('/admin/login/');
|
||||
}
|
||||
|
||||
return $admin;
|
||||
@@ -265,18 +285,18 @@ function app_handle_platform_admin_login(PDO $pdo): array
|
||||
$state = ['email' => $email];
|
||||
|
||||
if (!isset($_SESSION['admin_csrf']) || !hash_equals((string) $_SESSION['admin_csrf'], $csrf)) {
|
||||
$error = 'CSRF-Status ungueltig. Bitte Seite neu laden.';
|
||||
$error = 'Der Sicherheitsstatus ist ungültig. Bitte lade die Seite neu.';
|
||||
} elseif ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$error = 'Bitte eine gueltige E-Mail-Adresse angeben.';
|
||||
$error = 'Bitte gib eine gültige E-Mail-Adresse an.';
|
||||
} elseif ($password === '') {
|
||||
$error = 'Bitte das Passwort eingeben.';
|
||||
$error = 'Bitte gib dein Passwort ein.';
|
||||
} else {
|
||||
$admin = app_platform_admin_by_email($pdo, $email);
|
||||
|
||||
if ($admin === null) {
|
||||
$error = 'Zu dieser E-Mail-Adresse wurde kein Global-Admin gefunden.';
|
||||
} elseif (!app_verify_password((string) ($admin['password_hash'] ?? ''), $password)) {
|
||||
$error = 'Das Passwort ist ungueltig oder fuer diesen Global-Admin noch nicht gesetzt.';
|
||||
$error = 'Das Passwort ist ungültig oder für diesen Global-Admin noch nicht gesetzt.';
|
||||
} else {
|
||||
app_set_admin_user([
|
||||
'user_id' => $admin['id'],
|
||||
@@ -286,7 +306,7 @@ function app_handle_platform_admin_login(PDO $pdo): array
|
||||
]);
|
||||
app_clear_admin_login_state();
|
||||
app_flash('Global-Admin erfolgreich angemeldet.', 'success');
|
||||
app_redirect('/admin/tenants');
|
||||
app_redirect('/admin/');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,11 +354,11 @@ function app_upsert_tenant(PDO $pdo, array $data): void
|
||||
$tenantId = trim((string) ($data['tenant_id'] ?? ''));
|
||||
|
||||
if ($tenantKey === '' || !preg_match('/^[a-z0-9-]+$/', $tenantKey)) {
|
||||
throw new RuntimeException('Der Tenant-Key darf nur Kleinbuchstaben, Zahlen und Bindestriche enthalten.');
|
||||
throw new RuntimeException('Der Mandanten-Key darf nur Kleinbuchstaben, Zahlen und Bindestriche enthalten.');
|
||||
}
|
||||
|
||||
if ($name === '') {
|
||||
throw new RuntimeException('Bitte einen Tenant-Namen angeben.');
|
||||
throw new RuntimeException('Bitte gib einen Namen für den Mandanten an.');
|
||||
}
|
||||
|
||||
if (!in_array($status, ['active', 'inactive', 'sandbox'], true)) {
|
||||
@@ -352,7 +372,7 @@ function app_upsert_tenant(PDO $pdo, array $data): void
|
||||
);
|
||||
|
||||
if ($existing !== null) {
|
||||
throw new RuntimeException('Der Tenant-Key ist bereits vergeben.');
|
||||
throw new RuntimeException('Der Mandanten-Key ist bereits vergeben.');
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
@@ -370,7 +390,7 @@ function app_upsert_tenant(PDO $pdo, array $data): void
|
||||
]
|
||||
);
|
||||
|
||||
app_flash('Tenant wurde aktualisiert.', 'success');
|
||||
app_flash('Der Mandant wurde aktualisiert.', 'success');
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -388,7 +408,7 @@ function app_upsert_tenant(PDO $pdo, array $data): void
|
||||
]
|
||||
);
|
||||
|
||||
app_flash('Tenant wurde angelegt.', 'success');
|
||||
app_flash('Der Mandant wurde angelegt.', 'success');
|
||||
}
|
||||
|
||||
function app_handle_platform_tenant_action(PDO $pdo): void
|
||||
@@ -416,7 +436,7 @@ function app_handle_platform_tenant_action(PDO $pdo): void
|
||||
app_flash($exception->getMessage(), 'error');
|
||||
}
|
||||
|
||||
app_redirect('/admin/tenants');
|
||||
app_redirect('/admin/tenants/');
|
||||
}
|
||||
|
||||
function app_memberships_by_email(PDO $pdo, string $email): array
|
||||
@@ -478,12 +498,12 @@ function app_handle_login(PDO $pdo): array
|
||||
$email = strtolower(trim((string) ($_POST['email'] ?? '')));
|
||||
|
||||
if ($email === '') {
|
||||
$error = 'Bitte zuerst die E-Mail-Adresse eingeben.';
|
||||
$error = 'Bitte gib zuerst deine E-Mail-Adresse ein.';
|
||||
} else {
|
||||
$memberships = app_memberships_by_email($pdo, $email);
|
||||
|
||||
if ($memberships === []) {
|
||||
$error = 'Zu dieser E-Mail-Adresse wurde kein Zugang gefunden. Bitte Einladung oder Support pruefen.';
|
||||
$error = 'Zu dieser E-Mail-Adresse wurde kein Zugang gefunden. Bitte prüfe deine Einladung oder kontaktiere den Support.';
|
||||
app_clear_login_state();
|
||||
} elseif (count($memberships) === 1) {
|
||||
app_set_login_state([
|
||||
@@ -492,7 +512,7 @@ function app_handle_login(PDO $pdo): array
|
||||
'selected_membership' => $memberships[0],
|
||||
]);
|
||||
$state = app_login_state();
|
||||
$message = 'Tenant erkannt. Bitte jetzt mit Passwort fortfahren.';
|
||||
$message = 'Dein Bereich wurde erkannt. Bitte gib jetzt dein Passwort ein.';
|
||||
} else {
|
||||
app_set_login_state([
|
||||
'step' => 'choose-tenant',
|
||||
@@ -500,7 +520,7 @@ function app_handle_login(PDO $pdo): array
|
||||
'memberships' => $memberships,
|
||||
]);
|
||||
$state = app_login_state();
|
||||
$message = 'Mehrere Tenant-Zuordnungen erkannt. Bitte zuerst den gewuenschten Bereich auswaehlen.';
|
||||
$message = 'Zu deiner E-Mail-Adresse wurden mehrere Bereiche gefunden. Bitte wähle zuerst den passenden Bereich aus.';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -519,7 +539,7 @@ function app_handle_login(PDO $pdo): array
|
||||
}
|
||||
|
||||
if (!is_array($selected)) {
|
||||
$error = 'Die ausgewaehlte Tenant-Zuordnung konnte nicht gefunden werden.';
|
||||
$error = 'Die ausgewählte Zuordnung konnte nicht gefunden werden.';
|
||||
} else {
|
||||
app_set_login_state([
|
||||
'step' => 'password',
|
||||
@@ -528,7 +548,7 @@ function app_handle_login(PDO $pdo): array
|
||||
'memberships' => $memberships,
|
||||
]);
|
||||
$state = app_login_state();
|
||||
$message = 'Bereich ausgewaehlt. Bitte jetzt mit Passwort fortfahren.';
|
||||
$message = 'Bereich ausgewählt. Bitte gib jetzt dein Passwort ein.';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,19 +558,19 @@ function app_handle_login(PDO $pdo): array
|
||||
$selected = $state['selected_membership'] ?? null;
|
||||
|
||||
if (!is_array($selected)) {
|
||||
$error = 'Der Anmeldekontext ist abgelaufen. Bitte erneut mit der E-Mail-Adresse starten.';
|
||||
$error = 'Der Anmeldekontext ist abgelaufen. Bitte starte die Anmeldung erneut mit deiner E-Mail-Adresse.';
|
||||
} elseif ($password === '') {
|
||||
$error = 'Bitte jetzt das Passwort eingeben.';
|
||||
$error = 'Bitte gib jetzt dein Passwort ein.';
|
||||
} else {
|
||||
$passwordHash = $selected['password_hash'] ?? null;
|
||||
|
||||
if (!app_verify_password(is_string($passwordHash) ? $passwordHash : null, $password)) {
|
||||
$error = 'Das Passwort konnte nicht bestaetigt werden oder fuer diesen Benutzer ist noch kein Passwort gesetzt.';
|
||||
$error = 'Das Passwort konnte nicht bestätigt werden oder für dieses Benutzerkonto ist noch kein Passwort gesetzt.';
|
||||
} else {
|
||||
app_finalize_login($selected);
|
||||
app_flash('Anmeldung erfolgreich. Willkommen in ' . ($selected['tenant_name'] ?? 'deinem Bereich') . '.', 'success');
|
||||
app_clear_login_state();
|
||||
app_redirect('/dashboard');
|
||||
app_redirect('/dashboard/');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -583,9 +603,9 @@ function app_tenant_overview(PDO $pdo): array
|
||||
{
|
||||
$metrics = [
|
||||
['label' => 'Aktive Tenants', 'value' => '0', 'detail' => 'Mandanten mit aktivem Produktbetrieb.'],
|
||||
['label' => 'Mitglieder gesamt', 'value' => '0', 'detail' => 'Aktive Mitgliedschaften ueber alle Tenants.'],
|
||||
['label' => 'SSO-Abdeckung', 'value' => '0', 'detail' => 'Aktive OIDC-Provider fuer zentrale oder tenantbezogene Anmeldungen.'],
|
||||
['label' => 'Betriebsstatus', 'value' => 'Live', 'detail' => 'Die zentrale Runtime laeuft direkt aus public/.'],
|
||||
['label' => 'Mitglieder gesamt', 'value' => '0', 'detail' => 'Aktive Mitgliedschaften über alle Tenants.'],
|
||||
['label' => 'SSO-Abdeckung', 'value' => '0', 'detail' => 'Aktive OIDC-Provider für zentrale oder tenantbezogene Anmeldungen.'],
|
||||
['label' => 'Betriebsstatus', 'value' => 'Live', 'detail' => 'Die zentrale Runtime läuft direkt aus `public/`.'],
|
||||
];
|
||||
|
||||
$metricSql = <<<'SQL'
|
||||
@@ -680,18 +700,18 @@ SQL;
|
||||
'shared_access' => $sharedAccess,
|
||||
'operations' => [
|
||||
[
|
||||
'title' => 'Zentrale Anmeldung stabil halten',
|
||||
'detail' => 'Tenant-Erkennung, Mehrfachzuordnung und Passwort-/SSO-Pfade aus einer Hand pflegen.',
|
||||
'title' => 'Zentrale Anmeldung zuverlässig steuern',
|
||||
'detail' => 'Tenant-Erkennung, Mehrfachzuordnung und Passwort- oder SSO-Wege bleiben an einer Stelle gepflegt.',
|
||||
'state' => 'Live',
|
||||
],
|
||||
[
|
||||
'title' => 'Legacy-Kernprozesse tenantweise nachziehen',
|
||||
'detail' => 'Dashboard, Mitglieder, Ledger und Zahlungen Schritt fuer Schritt produktiv weiter ausbauen.',
|
||||
'state' => 'Naechster Schritt',
|
||||
'title' => 'Tenant-Verwaltung im Betrieb ausbauen',
|
||||
'detail' => 'Dashboard, Mitglieder, Buchungen und Einzahlungen wachsen Schritt für Schritt zur vollständigen Verwaltungsoberfläche.',
|
||||
'state' => 'Nächster Schritt',
|
||||
],
|
||||
[
|
||||
'title' => 'Marketing und Onboarding schaerfen',
|
||||
'detail' => 'Landingpage, Testzugang und Produktnutzen fuer neue Mieter klarer kommunizieren.',
|
||||
'title' => 'Marketing und Onboarding schärfen',
|
||||
'detail' => 'Landingpage, Testzugang und Nutzenversprechen sprechen Betreiber und Mitglieder klarer an.',
|
||||
'state' => 'Laufend',
|
||||
],
|
||||
],
|
||||
@@ -905,15 +925,15 @@ function app_handle_tenant_action(PDO $pdo, array $auth): void
|
||||
}
|
||||
|
||||
if (!app_can_manage_tenant($auth)) {
|
||||
app_flash('Fuer diese Aktion brauchst du Tenant-Admin-Rechte.', 'warning');
|
||||
app_redirect('/dashboard');
|
||||
app_flash('Für diese Aktion brauchst du Tenant-Admin-Rechte.', 'warning');
|
||||
app_redirect('/dashboard/');
|
||||
}
|
||||
|
||||
$memberId = trim((string) ($_POST['member_id'] ?? ''));
|
||||
$member = app_member_exists($pdo, $tenantId, $memberId);
|
||||
|
||||
if ($member === null) {
|
||||
app_flash('Das ausgewaehlte Mitglied konnte in diesem Tenant nicht gefunden werden.', 'error');
|
||||
app_flash('Das ausgewählte Mitglied konnte in diesem Tenant nicht gefunden werden.', 'error');
|
||||
app_redirect(app_request_path());
|
||||
}
|
||||
|
||||
@@ -929,7 +949,7 @@ function app_handle_tenant_action(PDO $pdo, array $auth): void
|
||||
$unitPrice = (float) ($_POST['unit_price'] ?? 0);
|
||||
|
||||
if ($strokes < 1 || $unitPrice <= 0) {
|
||||
throw new RuntimeException('Bitte mindestens einen Strich und einen gueltigen Preis angeben.');
|
||||
throw new RuntimeException('Bitte gib mindestens einen Strich und einen gültigen Preis an.');
|
||||
}
|
||||
|
||||
$entryId = app_uuid();
|
||||
@@ -973,8 +993,8 @@ function app_handle_tenant_action(PDO $pdo, array $auth): void
|
||||
);
|
||||
|
||||
$pdo->commit();
|
||||
app_flash($strokes . ' Strich(e) fuer ' . $member['display_name'] . ' wurden eingetragen.', 'success');
|
||||
app_redirect('/ledger');
|
||||
app_flash($strokes . ' Strich(e) für ' . $member['display_name'] . ' wurden eingetragen.', 'success');
|
||||
app_redirect('/ledger/');
|
||||
}
|
||||
|
||||
if ($action === 'record-payment') {
|
||||
@@ -983,7 +1003,7 @@ function app_handle_tenant_action(PDO $pdo, array $auth): void
|
||||
$method = $method !== '' ? $method : 'manual';
|
||||
|
||||
if ($amount <= 0) {
|
||||
throw new RuntimeException('Bitte einen gueltigen Einzahlungsbetrag angeben.');
|
||||
throw new RuntimeException('Bitte gib einen gültigen Einzahlungsbetrag an.');
|
||||
}
|
||||
|
||||
$entryId = app_uuid();
|
||||
@@ -1022,8 +1042,8 @@ function app_handle_tenant_action(PDO $pdo, array $auth): void
|
||||
);
|
||||
|
||||
$pdo->commit();
|
||||
app_flash('Die Einzahlung fuer ' . $member['display_name'] . ' wurde gebucht.', 'success');
|
||||
app_redirect('/payments');
|
||||
app_flash('Die Einzahlung für ' . $member['display_name'] . ' wurde gebucht.', 'success');
|
||||
app_redirect('/payments/');
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
@@ -1040,12 +1060,12 @@ function app_handle_tenant_action(PDO $pdo, array $auth): void
|
||||
function app_marketing_messages(): array
|
||||
{
|
||||
return [
|
||||
'hero' => 'Die tenantfaehige Kaffeeliste fuer Teams, Standorte und geteilte Services.',
|
||||
'subhero' => 'Zentrale Anmeldung, transparente Kontostaende, Einzahlungen, Striche und Hinweise in einer modernen, webspace-tauglichen SaaS-Oberflaeche.',
|
||||
'hero' => 'Die moderne Kaffeeliste für Teams, Standorte und gemeinsame Services.',
|
||||
'subhero' => 'Zentrale Anmeldung, transparente Kontostände, Einzahlungen, Buchungen und Hinweise in einer webbasierten SaaS-Lösung, die sich leicht betreiben lässt.',
|
||||
'benefits' => [
|
||||
'Ein Login fuer alle Mitgliedschaften statt verstreuter Tenant-URLs.',
|
||||
'Klare Admin-Console fuer Mandanten, Domains und Mehrfachzuordnungen.',
|
||||
'Kernprozesse aus der alten Kaffeeliste tenantfaehig und zentral steuerbar.',
|
||||
'Ein Login für alle Mitgliedschaften statt verteilter Tenant-URLs.',
|
||||
'Eine zentrale Verwaltung für Tenants, Zugänge und Betriebsübersicht.',
|
||||
'Klare Abläufe für Buchungen, Einzahlungen, Hinweise und Mitgliederverwaltung.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
+53
-53
@@ -135,8 +135,8 @@ $selectedMembership = $loginState['selected_membership'] ?? null;
|
||||
$restrictedPages = ['members', 'ledger', 'payments'];
|
||||
|
||||
if ($auth !== null && in_array($page, $restrictedPages, true) && !app_can_manage_tenant($auth)) {
|
||||
app_flash('Dieser Bereich ist nur fuer Tenant-Admins verfuegbar.', 'warning');
|
||||
app_redirect('/dashboard');
|
||||
app_flash('Dieser Bereich ist nur für Tenant-Admins verfügbar.', 'warning');
|
||||
app_redirect('/dashboard/');
|
||||
}
|
||||
|
||||
$canManageTenant = app_can_manage_tenant($auth);
|
||||
@@ -157,21 +157,21 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
<header class="bar">
|
||||
<div class="brand">
|
||||
<strong>Kaffeeliste SaaS</strong>
|
||||
<span>Zentrale Anmeldung, Tenant-Console und tenantweise Kaffeelisten-Funktionen.</span>
|
||||
<span>Zentrale Anmeldung, Tenant-Verwaltung und alle Kernfunktionen der Kaffeeliste in einer Oberfläche.</span>
|
||||
</div>
|
||||
<nav class="links" aria-label="Navigation">
|
||||
<a href="/" class="<?= $page === 'home' ? 'active' : '' ?>">Start</a>
|
||||
<a href="/login" class="<?= $page === 'login' ? 'active' : '' ?>">Anmeldung</a>
|
||||
<a href="<?= $auth !== null && app_is_platform_admin($auth) ? '/admin/tenants' : '/admin/login' ?>" class="<?= $page === 'tenants' ? 'active' : '' ?>"><?= $auth === null ? 'Fuer Betreiber' : 'Betreiber-Uebersicht' ?></a>
|
||||
<a href="/login/" class="<?= $page === 'login' ? 'active' : '' ?>">Anmeldung</a>
|
||||
<a href="<?= $auth !== null && app_is_platform_admin($auth) ? '/admin/' : '/admin/login/' ?>" class="<?= $page === 'tenants' ? 'active' : '' ?>"><?= $auth === null ? 'Für Betreiber' : 'Betreiber-Übersicht' ?></a>
|
||||
<?php if ($auth !== null): ?>
|
||||
<a href="/dashboard" class="<?= $page === 'dashboard' ? 'active' : '' ?>">Dashboard</a>
|
||||
<a href="/content" class="<?= $page === 'content' ? 'active' : '' ?>">Hinweise</a>
|
||||
<a href="/dashboard/" class="<?= $page === 'dashboard' ? 'active' : '' ?>">Dashboard</a>
|
||||
<a href="/content/" class="<?= $page === 'content' ? 'active' : '' ?>">Hinweise</a>
|
||||
<?php if ($canManageTenant): ?>
|
||||
<a href="/members" class="<?= $page === 'members' ? 'active' : '' ?>">Mitglieder</a>
|
||||
<a href="/ledger" class="<?= $page === 'ledger' ? 'active' : '' ?>">Buchungen</a>
|
||||
<a href="/payments" class="<?= $page === 'payments' ? 'active' : '' ?>">Einzahlungen</a>
|
||||
<a href="/members/" class="<?= $page === 'members' ? 'active' : '' ?>">Mitglieder</a>
|
||||
<a href="/ledger/" class="<?= $page === 'ledger' ? 'active' : '' ?>">Buchungen</a>
|
||||
<a href="/payments/" class="<?= $page === 'payments' ? 'active' : '' ?>">Einzahlungen</a>
|
||||
<?php endif; ?>
|
||||
<form method="post" action="/logout"><button type="submit" class="button secondary">Abmelden</button></form>
|
||||
<form method="post" action="/logout/"><button type="submit" class="button secondary">Abmelden</button></form>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -181,7 +181,7 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
<div class="context">
|
||||
<?= badge((string) $auth['tenant_name']) ?>
|
||||
<span class="muted"><?= h((string) $auth['display_name']) ?> ist angemeldet als <?= h((string) $auth['email']) ?></span>
|
||||
<?= app_is_platform_admin($auth) ? badge('Platform Admin', 'success') : (app_is_tenant_admin($auth) ? badge('Tenant Admin', 'success') : badge('Mitglied')) ?>
|
||||
<?= app_is_platform_admin($auth) ? badge('Global-Admin', 'success') : (app_is_tenant_admin($auth) ? badge('Tenant-Admin', 'success') : badge('Mitglied')) ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
@@ -192,12 +192,12 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
|
||||
<?php if ($page === 'home'): ?>
|
||||
<section class="hero">
|
||||
<div class="eyebrow">Marketing und Endanwender</div>
|
||||
<div class="eyebrow">Für Mitglieder und Betreiber</div>
|
||||
<h1><?= h((string) $marketing['hero']) ?></h1>
|
||||
<p><?= h((string) $marketing['subhero']) ?></p>
|
||||
<div class="actions" style="margin-top:18px">
|
||||
<a class="button" href="/login">Mitglieder anmelden</a>
|
||||
<a class="button secondary" href="/admin/login">Fuer Betreiber anmelden</a>
|
||||
<a class="button" href="/login/">Mitglieder anmelden</a>
|
||||
<a class="button secondary" href="/admin/login/">Als Global-Admin anmelden</a>
|
||||
</div>
|
||||
<ul class="list">
|
||||
<?php foreach (($marketing['benefits'] ?? []) as $benefit): ?>
|
||||
@@ -219,9 +219,9 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="grid grid-3" style="margin-top:18px">
|
||||
<article class="card"><h2>Fuer Mitglieder</h2><p>Zentral mit E-Mail starten, Tenant erkennen lassen und direkt den eigenen Kontostand oder die letzten Buchungen sehen.</p></article>
|
||||
<article class="card"><h2>Fuer Tenant-Admins</h2><p>Mitglieder, Striche, Einzahlungen und Hinweise bleiben tenantweise sauber getrennt und professionell bedienbar.</p></article>
|
||||
<article class="card"><h2>Fuer Betreiber</h2><p>Das Produkt wirkt vermietbar, weil Portfolio, Routing und Betrieb des Gesamtangebots an einer Stelle sichtbar werden.</p></article>
|
||||
<article class="card"><h2>Für Mitglieder</h2><p>Mit der geschäftlichen E-Mail einsteigen, den passenden Bereich automatisch erkennen lassen und den eigenen Stand sofort sehen.</p></article>
|
||||
<article class="card"><h2>Für Tenant-Admins</h2><p>Mitglieder, Buchungen, Einzahlungen und Hinweise bleiben pro Tenant sauber getrennt und leicht zu verwalten.</p></article>
|
||||
<article class="card"><h2>Für Betreiber</h2><p>Die Plattform wirkt professionell, weil Verwaltung, Anmeldung und Produktpräsentation an einer Stelle zusammenlaufen.</p></article>
|
||||
</section>
|
||||
|
||||
<?php if ($dbError !== null): ?>
|
||||
@@ -230,10 +230,10 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
<?php elseif ($page === 'login'): ?>
|
||||
<section class="hero">
|
||||
<div class="eyebrow">Zentrale Anmeldung</div>
|
||||
<h1>Mitglieder starten jetzt wirklich E-Mail first.</h1>
|
||||
<p>Erst Tenant finden, dann bei Bedarf zwischen mehreren Zugehoerigkeiten waehlen und erst danach das Passwort eingeben.</p>
|
||||
<h1>Mitglieder melden sich zentral mit ihrer E-Mail-Adresse an.</h1>
|
||||
<p>Zuerst wird der passende Bereich ermittelt. Falls mehrere Zuordnungen vorhanden sind, wählst du den richtigen Tenant aus und gibst danach dein Passwort ein.</p>
|
||||
<div class="context" style="margin-top:16px">
|
||||
<?= badge('E-Mail first') ?>
|
||||
<?= badge('E-Mail zuerst') ?>
|
||||
<?= badge('Tenant-Erkennung') ?>
|
||||
<?= badge('Mehrfachzuordnung') ?>
|
||||
</div>
|
||||
@@ -250,16 +250,16 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
<article class="card">
|
||||
<?php if ($loginStep === 'choose-tenant'): ?>
|
||||
<div class="eyebrow">Schritt 2</div>
|
||||
<h2>Passenden Tenant waehlen</h2>
|
||||
<h2>Passenden Tenant wählen</h2>
|
||||
<div class="stack">
|
||||
<?php foreach (($loginState['memberships'] ?? []) as $membership): ?>
|
||||
<form method="post" action="/login" class="card">
|
||||
<form method="post" action="/login/" class="card">
|
||||
<input type="hidden" name="action" value="choose-tenant">
|
||||
<input type="hidden" name="tenant_user_id" value="<?= h((string) ($membership['tenant_user_id'] ?? '')) ?>">
|
||||
<h3><?= h((string) ($membership['tenant_name'] ?? 'Tenant')) ?></h3>
|
||||
<p><?= h((string) ($membership['display_name'] ?? $membership['user_email'] ?? '')) ?></p>
|
||||
<p class="muted"><?= h((string) ($membership['role_keys'] ?? 'Mitglied')) ?></p>
|
||||
<div class="actions" style="margin-top:12px"><button type="submit">Diesen Tenant oeffnen</button></div>
|
||||
<div class="actions" style="margin-top:12px"><button type="submit">Diesen Tenant öffnen</button></div>
|
||||
</form>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
@@ -267,18 +267,18 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
<div class="eyebrow">Schritt 2</div>
|
||||
<h2>Passwort eingeben</h2>
|
||||
<p>Der Tenant wurde erkannt: <?= h((string) ($selectedMembership['tenant_name'] ?? '')) ?></p>
|
||||
<form method="post" action="/login" class="stack" style="margin-top:16px">
|
||||
<form method="post" action="/login/" class="stack" style="margin-top:16px">
|
||||
<input type="hidden" name="action" value="authenticate">
|
||||
<label>Passwort<input type="password" name="password" autocomplete="current-password" placeholder="Passwort eingeben"></label>
|
||||
<div class="actions">
|
||||
<button type="submit">Anmelden</button>
|
||||
<a class="button secondary" href="/login">Neu starten</a>
|
||||
<a class="button secondary" href="/login/">Neu starten</a>
|
||||
</div>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<div class="eyebrow">Schritt 1</div>
|
||||
<h2>E-Mail eingeben</h2>
|
||||
<form method="post" action="/login" class="stack" style="margin-top:16px">
|
||||
<form method="post" action="/login/" class="stack" style="margin-top:16px">
|
||||
<input type="hidden" name="action" value="discover">
|
||||
<label>E-Mail-Adresse<input type="email" name="email" value="<?= h((string) ($loginState['email'] ?? '')) ?>" autocomplete="email" placeholder="mitglied@unternehmen.de"></label>
|
||||
<div class="actions"><button type="submit">Weiter</button></div>
|
||||
@@ -289,17 +289,17 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
<div class="eyebrow">Hilfe beim Einstieg</div>
|
||||
<h2>Wenn die Anmeldung nicht sofort klappt</h2>
|
||||
<ul class="list">
|
||||
<li>Pruefe zuerst, ob du die richtige berufliche E-Mail-Adresse verwendest.</li>
|
||||
<li>Wenn du in mehreren Bereichen arbeitest, wirst du automatisch zur passenden Auswahl gefuehrt.</li>
|
||||
<li>Falls kein Zugang gefunden wird, brauchst du meist nur eine Einladung oder den Kontakt zum verantwortlichen Betreiber.</li>
|
||||
<li>Prüfe zuerst, ob du die richtige berufliche E-Mail-Adresse verwendest.</li>
|
||||
<li>Wenn du in mehreren Bereichen arbeitest, wirst du automatisch zur passenden Auswahl geführt.</li>
|
||||
<li>Falls kein Zugang gefunden wird, brauchst du meist nur eine Einladung oder den Kontakt zur verantwortlichen Person.</li>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
<?php elseif ($page === 'tenants'): ?>
|
||||
<section class="hero">
|
||||
<div class="eyebrow">Betreiber-Sicht</div>
|
||||
<h1>Die Tenant-Console zeigt Portfolio, Login-Wege und Mehrfachzugriffe in einer professionellen Uebersicht.</h1>
|
||||
<p>Die Landingpage verkauft das Produkt, die Anmeldung dient Mitgliedern und diese Sicht bleibt die zentrale Betreiber-Ebene.</p>
|
||||
<h1>Die Tenant-Übersicht bündelt Portfolio, Zugänge und Betriebsstatus an einem Ort.</h1>
|
||||
<p>Hier sehen Betreiber, wie viele Tenants aktiv sind, wie die Anmeldung funktioniert und wo Mehrfachzuordnungen sauber abgefangen werden.</p>
|
||||
</section>
|
||||
|
||||
<?php if ($tenantOverview !== null): ?>
|
||||
@@ -325,7 +325,7 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
<td><?= num($tenant['member_count'] ?? 0) ?></td>
|
||||
<td><?= num($tenant['admin_count'] ?? 0) ?></td>
|
||||
<td><?= h((string) $tenant['login_mode']) ?></td>
|
||||
<td><?= ((string) ($tenant['status'] ?? 'active')) === 'active' ? badge('Aktiv', 'success') : badge('Pruefen', 'warning') ?></td>
|
||||
<td><?= ((string) ($tenant['status'] ?? 'active')) === 'active' ? badge('Aktiv', 'success') : badge('Prüfen', 'warning') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
@@ -339,22 +339,22 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
<section class="hero">
|
||||
<div class="eyebrow">Tenant-Dashboard</div>
|
||||
<h1><?= h((string) $auth['tenant_name']) ?> auf einen Blick</h1>
|
||||
<p>Kontostand, Striche, Einzahlungen und die letzten Buchungen sind damit direkt tenantweise verfuegbar.</p>
|
||||
<p>Kontostand, Buchungen, Einzahlungen und die letzten Aktivitäten stehen direkt für diesen Tenant bereit.</p>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-4">
|
||||
<article class="metric"><strong><?= num($tenantDashboard['active_members'] ?? 0) ?></strong><h3>Aktive Mitglieder</h3><p>Aktive Mitgliedschaften im Tenant.</p></article>
|
||||
<article class="metric"><strong><?= money($tenantDashboard['coffee_volume'] ?? 0) ?></strong><h3>Kaffeeverbrauch</h3><p>Summe der Kaffee-Buchungen.</p></article>
|
||||
<article class="metric"><strong><?= money($tenantDashboard['payment_volume'] ?? 0) ?></strong><h3>Einzahlungen</h3><p>Summe der Zahlungsbuchungen.</p></article>
|
||||
<article class="metric"><strong><?= money($memberSummary['balance'] ?? 0) ?></strong><h3>Mein Kontostand</h3><p>Saldo fuer den aktuell angemeldeten Nutzer.</p></article>
|
||||
<article class="metric"><strong><?= money($memberSummary['balance'] ?? 0) ?></strong><h3>Mein Kontostand</h3><p>Saldo für den aktuell angemeldeten Nutzer.</p></article>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-2" style="margin-top:18px">
|
||||
<?php if ($canManageTenant): ?>
|
||||
<article class="card">
|
||||
<div class="eyebrow">Legacy-Kernfunktion</div>
|
||||
<h2>Striche verbuchen</h2>
|
||||
<form method="post" action="/dashboard" class="grid">
|
||||
<div class="eyebrow">Kernfunktion</div>
|
||||
<h2>Buchungen für Kaffee erfassen</h2>
|
||||
<form method="post" action="/dashboard/" class="grid">
|
||||
<input type="hidden" name="action" value="record-coffee">
|
||||
<label>Mitglied<select name="member_id"><?php foreach ($members as $member): ?><option value="<?= h((string) $member['id']) ?>" <?= ((string) ($auth['member_id'] ?? '')) === (string) $member['id'] ? 'selected' : '' ?>><?= h((string) $member['display_name']) ?></option><?php endforeach; ?></select></label>
|
||||
<label>Anzahl Striche<input type="number" name="strokes" min="1" step="1" value="1"></label>
|
||||
@@ -365,9 +365,9 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
</form>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="eyebrow">Legacy-Kernfunktion</div>
|
||||
<div class="eyebrow">Kernfunktion</div>
|
||||
<h2>Einzahlung erfassen</h2>
|
||||
<form method="post" action="/dashboard" class="grid">
|
||||
<form method="post" action="/dashboard/" class="grid">
|
||||
<input type="hidden" name="action" value="record-payment">
|
||||
<label>Mitglied<select name="member_id"><?php foreach ($members as $member): ?><option value="<?= h((string) $member['id']) ?>" <?= ((string) ($auth['member_id'] ?? '')) === (string) $member['id'] ? 'selected' : '' ?>><?= h((string) $member['display_name']) ?></option><?php endforeach; ?></select></label>
|
||||
<label>Betrag<input type="number" name="amount" min="0.01" step="0.01" value="5.00"></label>
|
||||
@@ -382,15 +382,15 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
<h2>Dein Bereich</h2>
|
||||
<ul class="list">
|
||||
<li>Hier siehst du deinen Kontostand, letzte Buchungen und aktuelle Hinweise.</li>
|
||||
<li>Tenant-weite Buchungen und Mitgliedsverwaltung bleiben bei den verantwortlichen Admins.</li>
|
||||
<li>Tenant-weite Buchungen und Mitgliederverwaltung bleiben bei den verantwortlichen Admins.</li>
|
||||
<li>Wenn dir etwas fehlt, wende dich an den Betreiber deines Bereichs.</li>
|
||||
</ul>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="eyebrow">Naechster Schritt</div>
|
||||
<div class="eyebrow">Nächster Schritt</div>
|
||||
<h2>Womit du direkt weiterkommst</h2>
|
||||
<div class="actions">
|
||||
<a class="button" href="/content">Hinweise ansehen</a>
|
||||
<a class="button" href="/content/">Hinweise ansehen</a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endif; ?>
|
||||
@@ -406,14 +406,14 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
</div>
|
||||
</section>
|
||||
<?php elseif ($page === 'members'): ?>
|
||||
<section class="hero"><div class="eyebrow">Mitgliederverwaltung</div><h1>Mitglieder, Rollen und Aktivstatus</h1><p>Die Tenant-Variante der alten Mitarbeiterverwaltung mit Rollen und Benutzerbezug.</p></section>
|
||||
<section class="hero"><div class="eyebrow">Mitgliederverwaltung</div><h1>Mitglieder, Rollen und Aktivstatus</h1><p>Die zentrale Übersicht für Mitglieder, Rollen und den aktuellen Status innerhalb eines Tenants.</p></section>
|
||||
<section class="card"><div class="table"><table><thead><tr><th>Name</th><th>E-Mail</th><th>Status</th><th>Benutzer</th><th>Rollen</th></tr></thead><tbody><?php foreach ($members as $member): ?><tr><td><strong><?= h((string) $member['display_name']) ?></strong></td><td><?= h((string) $member['email']) ?></td><td><?= ((string) ($member['status'] ?? 'active')) === 'active' ? badge('Aktiv', 'success') : badge('Inaktiv', 'warning') ?></td><td><?= h((string) ($member['user_display_name'] ?? '')) ?></td><td><?= h((string) ($member['roles'] ?? 'Mitglied')) ?></td></tr><?php endforeach; ?></tbody></table></div></section>
|
||||
<?php elseif ($page === 'ledger'): ?>
|
||||
<section class="hero"><div class="eyebrow">Ledger</div><h1>Striche und Buchungen</h1><p>Kaffeeeintraege und Folgebuchungen bleiben tenantweise nachvollziehbar.</p></section>
|
||||
<section class="hero"><div class="eyebrow">Buchungen</div><h1>Striche und Buchungen</h1><p>Kaffeeeinträge und alle zugehörigen Buchungen bleiben tenantweise nachvollziehbar.</p></section>
|
||||
<section class="grid grid-2">
|
||||
<article class="card">
|
||||
<h2>Neuen Strich-Eintrag buchen</h2>
|
||||
<form method="post" action="/ledger" class="grid">
|
||||
<h2>Neuen Strich eintragen</h2>
|
||||
<form method="post" action="/ledger/" class="grid">
|
||||
<input type="hidden" name="action" value="record-coffee">
|
||||
<label>Mitglied<select name="member_id"><?php foreach ($members as $member): ?><option value="<?= h((string) $member['id']) ?>"><?= h((string) $member['display_name']) ?></option><?php endforeach; ?></select></label>
|
||||
<label>Anzahl Striche<input type="number" name="strokes" min="1" step="1" value="1"></label>
|
||||
@@ -422,7 +422,7 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
<div class="actions"><button type="submit">Buchen</button></div>
|
||||
</form>
|
||||
</article>
|
||||
<article class="card"><h2>Abgedeckte Alt-Funktionen</h2><ul class="list"><li>Sammel- und Self-Service-Striche sind in einer Logik zusammengefuehrt.</li><li>Jeder Verbrauch erzeugt direkt auch den Ledger-Eintrag.</li><li>Die letzten Buchungen sind tenantweise sichtbar.</li></ul></article>
|
||||
<article class="card"><h2>Was dieser Bereich abdeckt</h2><ul class="list"><li>Einzel- und Sammelbuchungen laufen in einem gemeinsamen Ablauf zusammen.</li><li>Jeder Verbrauch erzeugt automatisch den passenden Ledger-Eintrag.</li><li>Die letzten Buchungen bleiben je Tenant nachvollziehbar.</li></ul></article>
|
||||
</section>
|
||||
<section class="card" style="margin-top:18px"><div class="table"><table><thead><tr><th>Zeit</th><th>Mitglied</th><th>Typ</th><th>Referenz</th><th>Betrag</th></tr></thead><tbody><?php foreach ($ledger as $entry): ?><tr><td><?= dt((string) ($entry['booked_at'] ?? '')) ?></td><td><?= h((string) ($entry['member_name'] ?? '')) ?></td><td><?= h((string) ($entry['entry_type'] ?? '')) ?></td><td><?= h((string) ($entry['reference_type'] ?? '')) ?></td><td><?= money($entry['amount'] ?? 0) ?></td></tr><?php endforeach; ?></tbody></table></div></section>
|
||||
<?php elseif ($page === 'payments'): ?>
|
||||
@@ -430,7 +430,7 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
<section class="grid grid-2">
|
||||
<article class="card">
|
||||
<h2>Einzahlung buchen</h2>
|
||||
<form method="post" action="/payments" class="grid">
|
||||
<form method="post" action="/payments/" class="grid">
|
||||
<input type="hidden" name="action" value="record-payment">
|
||||
<label>Mitglied<select name="member_id"><?php foreach ($members as $member): ?><option value="<?= h((string) $member['id']) ?>"><?= h((string) $member['display_name']) ?></option><?php endforeach; ?></select></label>
|
||||
<label>Betrag<input type="number" name="amount" min="0.01" step="0.01" value="5.00"></label>
|
||||
@@ -439,20 +439,20 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
<div class="actions"><button type="submit">Einzahlung speichern</button></div>
|
||||
</form>
|
||||
</article>
|
||||
<article class="card"><h2>Wofuer dieser Bereich da ist</h2><ul class="list"><li>Manuelle Einzahlungen sind direkt erfasst.</li><li>PayPal oder Bank koennen getrennt ausgewiesen werden.</li><li>Jede Zahlung erscheint sofort im Ledger.</li></ul></article>
|
||||
<article class="card"><h2>Wofür dieser Bereich da ist</h2><ul class="list"><li>Manuelle Einzahlungen sind direkt erfasst.</li><li>PayPal oder Bank können getrennt ausgewiesen werden.</li><li>Jede Zahlung erscheint sofort im Ledger.</li></ul></article>
|
||||
</section>
|
||||
<section class="card" style="margin-top:18px"><div class="table"><table><thead><tr><th>Zeit</th><th>Mitglied</th><th>Methode</th><th>Betrag</th></tr></thead><tbody><?php foreach ($payments as $entry): ?><tr><td><?= dt((string) ($entry['booked_at'] ?? '')) ?></td><td><?= h((string) ($entry['member_name'] ?? '')) ?></td><td><?= h((string) ($entry['payment_method'] ?? '')) ?></td><td><?= money($entry['amount'] ?? 0) ?></td></tr><?php endforeach; ?></tbody></table></div></section>
|
||||
<?php elseif ($page === 'content'): ?>
|
||||
<section class="hero"><div class="eyebrow">Hinweise und FAQ</div><h1>Tenant-Inhalte zentral aus der Datenbank</h1><p>Hinweise und FAQ aus dem alten Programm sind jetzt als tenantbezogene Inhalte vorbereitet.</p></section>
|
||||
<section class="hero"><div class="eyebrow">Hinweise und FAQ</div><h1>Tenant-Inhalte zentral aus der Datenbank</h1><p>Hinweise und häufige Fragen werden pro Tenant gepflegt und direkt an die Mitglieder ausgespielt.</p></section>
|
||||
<section class="grid grid-2">
|
||||
<article class="card"><h2>Hinweise</h2><div class="stack"><?php if (($content['announcements'] ?? []) === []): ?><div class="metric"><p>Aktuell keine Hinweise vorhanden.</p></div><?php endif; ?><?php foreach (($content['announcements'] ?? []) as $entry): ?><div class="metric"><h3><?= h((string) $entry['title']) ?></h3><p><?= nl2br(h((string) $entry['message'])) ?></p><p class="muted">Sichtbar bis <?= dt((string) ($entry['visible_until'] ?? '')) ?></p></div><?php endforeach; ?></div></article>
|
||||
<article class="card"><h2>Hinweise</h2><div class="stack"><?php if (($content['announcements'] ?? []) === []): ?><div class="metric"><p>Aktuell sind keine Hinweise vorhanden.</p></div><?php endif; ?><?php foreach (($content['announcements'] ?? []) as $entry): ?><div class="metric"><h3><?= h((string) $entry['title']) ?></h3><p><?= nl2br(h((string) $entry['message'])) ?></p><p class="muted">Sichtbar bis <?= dt((string) ($entry['visible_until'] ?? '')) ?></p></div><?php endforeach; ?></div></article>
|
||||
<article class="card"><h2>FAQ</h2><div class="stack"><?php if (($content['faq'] ?? []) === []): ?><div class="metric"><p>Aktuell keine FAQ vorhanden.</p></div><?php endif; ?><?php foreach (($content['faq'] ?? []) as $entry): ?><div class="metric"><h3><?= h((string) $entry['question']) ?></h3><p><?= nl2br(h((string) $entry['answer'])) ?></p></div><?php endforeach; ?></div></article>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<section class="alert alert-warning">Die angeforderte Seite konnte nicht gefunden werden.</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="footer">Kaffeeliste SaaS | zentrale Anmeldung, Tenant-Console und tenantweise Kernprozesse fuer die moderne Kaffeeliste</p>
|
||||
<p class="footer">Kaffeeliste SaaS | zentrale Anmeldung, Tenant-Verwaltung und alle Kernprozesse der modernen Kaffeeliste</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+18
-16
@@ -2,7 +2,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
session_start();
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$bootstrapPath = __DIR__ . '/install-support.php';
|
||||
|
||||
@@ -85,7 +87,7 @@ if ($requestMethod === 'POST' && !$locked) {
|
||||
$csrf = (string) ($_POST['csrf'] ?? '');
|
||||
|
||||
if (!hash_equals($_SESSION['installer_csrf'], $csrf)) {
|
||||
$errors[] = 'Ungueltiger CSRF-Status. Seite neu laden.';
|
||||
$errors[] = 'Der Sicherheitsstatus ist ungültig. Bitte lade die Seite neu.';
|
||||
} else {
|
||||
foreach (array_keys($values) as $key) {
|
||||
if (array_key_exists($key, $_POST)) {
|
||||
@@ -138,7 +140,7 @@ if ($requestMethod === 'POST' && !$locked) {
|
||||
$messages[] = '.env wurde geschrieben.';
|
||||
|
||||
$bundlePath = scripts_build_migration_bundle(null, $values['DB_CONNECTION']);
|
||||
$messages[] = 'SQL-Bundle fuer ' . scripts_connection_label($values['DB_CONNECTION']) . ' wurde erzeugt.';
|
||||
$messages[] = 'Das SQL-Bundle für ' . scripts_connection_label($values['DB_CONNECTION']) . ' wurde erzeugt.';
|
||||
|
||||
if (isset($_POST['run_migrations'])) {
|
||||
$executedMigrations = scripts_run_sql_migrations([
|
||||
@@ -149,7 +151,7 @@ if ($requestMethod === 'POST' && !$locked) {
|
||||
'username' => $values['DB_USERNAME'],
|
||||
'password' => $values['DB_PASSWORD'],
|
||||
]);
|
||||
$messages[] = 'Migrationen wurden direkt ueber PHP fuer ' . $activeConnectionLabel . ' ausgefuehrt.';
|
||||
$messages[] = 'Die Schema-Migrationen wurden direkt über PHP für ' . $activeConnectionLabel . ' ausgeführt.';
|
||||
}
|
||||
|
||||
if (!$requiredExtensionLoaded) {
|
||||
@@ -187,19 +189,19 @@ if ($requestMethod === 'POST' && !$locked) {
|
||||
'app_url' => $values['APP_URL'],
|
||||
'db_database' => $values['DB_DATABASE'],
|
||||
]);
|
||||
$messages[] = 'Installer wurde gesperrt. Fuer eine erneute Ausfuehrung muss `saas-app/.installer.lock` entfernt werden.';
|
||||
$messages[] = 'Der Installer wurde gesperrt. Für eine erneute Ausführung muss `saas-app/.installer.lock` entfernt werden.';
|
||||
$locked = true;
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
$message = $exception->getMessage();
|
||||
|
||||
if (scripts_string_contains($message, 'pdo_')) {
|
||||
$errors[] = 'Migrationen konnten nicht direkt ueber PHP ausgefuehrt werden. Bitte `' . $requiredExtension . '` pruefen oder das SQL-Bundle manuell importieren.';
|
||||
$errors[] = 'Die Schema-Migrationen konnten nicht direkt über PHP ausgeführt werden. Bitte prüfe `' . $requiredExtension . '` oder importiere das SQL-Bundle manuell.';
|
||||
} elseif (scripts_string_contains($message, '.env')) {
|
||||
$errors[] = 'Die Konfiguration konnte nicht gespeichert werden. Zielpfad: ' . scripts_env_path();
|
||||
$errors[] = 'Status fuer .env: ' . scripts_path_state_label($envPathState) . '. Elternordner: ' . $envPathState['parent'];
|
||||
$errors[] = 'Status für .env: ' . scripts_path_state_label($envPathState) . '. Elternordner: ' . $envPathState['parent'];
|
||||
if (!$saasAppVisible) {
|
||||
$errors[] = 'PHP kann den SaaS-App-Ordner nicht lesen. Bitte Rechte, Document-Root und `open_basedir` pruefen.';
|
||||
$errors[] = 'PHP kann den SaaS-App-Ordner nicht lesen. Bitte prüfe Rechte, Document-Root und `open_basedir`.';
|
||||
}
|
||||
} else {
|
||||
$errors[] = 'Die Installation konnte nicht abgeschlossen werden: ' . $message;
|
||||
@@ -376,16 +378,16 @@ function h(string $value): string
|
||||
<body>
|
||||
<main class="shell">
|
||||
<section class="hero">
|
||||
<div class="eyebrow">One-time Installer</div>
|
||||
<h1>Kaffeeliste SaaS im Browser installieren.</h1>
|
||||
<div class="eyebrow">Browser-Installer</div>
|
||||
<h1>Kaffeeliste SaaS direkt im Browser einrichten.</h1>
|
||||
<p>
|
||||
Diese Seite fuehrt die Erstinstallation ueber den Webspace: `.env` schreiben,
|
||||
SQL-Bundle erzeugen und optional die Migrationen direkt ueber PHP ausfuehren.
|
||||
Diese Seite führt die Ersteinrichtung über den Webspace aus: `.env` schreiben,
|
||||
das Schema vorbereiten, ein SQL-Bundle erzeugen und den ersten Global-Admin anlegen.
|
||||
Nach erfolgreicher Einrichtung sollte der Installer gesperrt bleiben.
|
||||
</p>
|
||||
<div class="actions" style="margin-top: 18px;">
|
||||
<a class="link-button" href="./../index.php">Zur Preview</a>
|
||||
<a class="link-button" href="/admin/login">Zum Global-Admin</a>
|
||||
<a class="link-button" href="/admin/login/">Zum Global-Admin</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -412,7 +414,7 @@ function h(string $value): string
|
||||
<?php if ($locked): ?>
|
||||
<ul class="status-list">
|
||||
<li>Der Installer ist gesperrt.</li>
|
||||
<li>Fuer eine erneute Ausfuehrung muss <code>saas-app/.installer.lock</code> entfernt werden.</li>
|
||||
<li>Für eine erneute Ausführung muss <code>saas-app/.installer.lock</code> entfernt werden.</li>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<form method="post">
|
||||
@@ -518,7 +520,7 @@ function h(string $value): string
|
||||
<div class="stack">
|
||||
<label class="checkbox">
|
||||
<input name="run_migrations" type="checkbox"<?= $requiredExtensionLoaded ? '' : ' disabled' ?>>
|
||||
<span>Migrationen direkt ueber PHP fuer <?= h($activeConnectionLabel) ?> ausfuehren<?= $requiredExtensionLoaded ? '' : ' (' . h($requiredExtension) . ' fehlt aktuell)' ?></span>
|
||||
<span>Schema direkt über PHP für <?= h($activeConnectionLabel) ?> anlegen<?= $requiredExtensionLoaded ? '' : ' (' . h($requiredExtension) . ' fehlt aktuell)' ?></span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input name="lock_installer" type="checkbox" checked>
|
||||
@@ -563,7 +565,7 @@ function h(string $value): string
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($executedMigrations !== []): ?>
|
||||
<p style="margin-top: 18px;">Ausgefuehrte Migrationen:</p>
|
||||
<p style="margin-top: 18px;">Ausgeführte Migrationen:</p>
|
||||
<ul class="status-list">
|
||||
<?php foreach ($executedMigrations as $file): ?>
|
||||
<li><code><?= h($file) ?></code></li>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$_GET['page'] = 'logout';
|
||||
|
||||
require dirname(__DIR__) . '/index.php';
|
||||
Reference in New Issue
Block a user