Anpassung des Menü

This commit is contained in:
2026-03-30 17:36:47 +02:00
parent e6f7c0146f
commit f96d2bc681
20 changed files with 742 additions and 208 deletions
+190
View File
@@ -283,6 +283,196 @@ function app_can_manage_tenant(?array $auth): bool
return app_is_platform_admin($auth) || app_is_tenant_admin($auth);
}
function app_has_role(?array $auth, string $role): bool
{
if (!is_array($auth)) {
return false;
}
return in_array($role, $auth['roles'] ?? [], true);
}
function app_can_manage_finance(?array $auth): bool
{
return app_can_manage_tenant($auth) || app_has_role($auth, 'finance_admin');
}
function app_can_manage_support(?array $auth): bool
{
return app_can_manage_tenant($auth) || app_has_role($auth, 'support_contact');
}
function app_can_manage_surveys(?array $auth): bool
{
return app_can_manage_tenant($auth) || app_has_role($auth, 'survey_manager');
}
/**
* @return array<int, string>
*/
function app_auth_role_labels(?array $auth): array
{
if (!is_array($auth)) {
return [];
}
if (app_is_platform_admin($auth) && empty($auth['acting_as_platform_admin'])) {
return ['Global-Admin'];
}
$labels = [];
$map = [
'tenant_admin' => 'Tenant-Admin',
'finance_admin' => 'Finanzen',
'support_contact' => 'Support',
'survey_manager' => 'Umfragen',
];
foreach ($map as $role => $label) {
if (app_has_role($auth, $role)) {
$labels[] = $label;
}
}
if ($labels === []) {
$labels[] = 'Mitglied';
}
if (!empty($auth['acting_as_platform_admin'])) {
array_unshift($labels, 'Global-Admin');
}
return array_values(array_unique($labels));
}
function app_primary_role_label(?array $auth): string
{
$labels = app_auth_role_labels($auth);
return $labels[0] ?? 'Mitglied';
}
/**
* @return array<int, array{href:string,label:string}>
*/
function app_tenant_nav_items(?array $auth, array $features = []): array
{
if (!is_array($auth)) {
return [
['href' => '/', 'label' => 'Start'],
['href' => '/login/', 'label' => 'Anmeldung'],
['href' => '/admin/login/', 'label' => 'Betreiber'],
];
}
$items = [
['href' => '/dashboard/', 'label' => 'Dashboard'],
['href' => '/content/', 'label' => 'Hinweise'],
['href' => '/support/', 'label' => 'Support'],
['href' => '/surveys/', 'label' => 'Umfragen'],
];
if (app_can_manage_tenant($auth)) {
$items[] = ['href' => '/members/', 'label' => 'Mitglieder'];
}
if (app_can_manage_finance($auth)) {
$items[] = ['href' => '/ledger/', 'label' => 'Buchungen'];
$items[] = ['href' => '/payments/', 'label' => 'Einzahlungen'];
}
if (app_can_manage_tenant($auth)) {
$items[] = ['href' => '/tenants/roles/', 'label' => 'Rollen'];
}
if (app_can_manage_tenant($auth) && !empty($features['tenant_settings'])) {
$items[] = ['href' => '/settings/', 'label' => 'Einstellungen'];
}
if (
app_can_manage_tenant($auth)
&& (
!empty($features['pdf_export'])
|| !empty($features['paper_strike_entry'])
|| !empty($features['basic_exports'])
)
) {
$items[] = ['href' => '/exports/', 'label' => 'Exporte'];
}
return $items;
}
/**
* @param array<int, string> $roles
*/
function app_auth_has_any_role(?array $auth, array $roles): bool
{
if (!is_array($auth)) {
return false;
}
$granted = $auth['roles'] ?? [];
if (!is_array($granted)) {
return false;
}
foreach ($roles as $role) {
if (in_array($role, $granted, true)) {
return true;
}
}
return false;
}
/**
* @return array<int, array{key:string,label:string,href:string}>
*/
function app_tenant_navigation_items(?array $auth, array $license = []): array
{
if (!is_array($auth)) {
return [
['key' => 'home', 'label' => 'Start', 'href' => '/'],
['key' => 'login', 'label' => 'Anmeldung', 'href' => '/login/'],
['key' => 'admin', 'label' => 'Verwaltung', 'href' => '/admin/login/'],
];
}
$features = is_array($license['features'] ?? null) ? $license['features'] : [];
$canManage = app_can_manage_tenant($auth);
$canFinance = $canManage || app_auth_has_any_role($auth, ['finance_admin']);
$hasExports = !empty($features['pdf_export']) || !empty($features['paper_strike_entry']) || !empty($features['basic_exports']);
$items = [
['key' => 'dashboard', 'label' => 'Uebersicht', 'href' => '/dashboard/'],
['key' => 'content', 'label' => 'Hinweise', 'href' => '/content/'],
['key' => 'support', 'label' => 'Support', 'href' => '/support/'],
['key' => 'surveys', 'label' => 'Umfragen', 'href' => '/surveys/'],
];
if ($canFinance) {
$items[] = ['key' => 'ledger', 'label' => 'Buchungen', 'href' => '/ledger/'];
$items[] = ['key' => 'payments', 'label' => 'Zahlungen', 'href' => '/payments/'];
}
if ($canManage) {
$items[] = ['key' => 'members', 'label' => 'Mitglieder', 'href' => '/members/'];
$items[] = ['key' => 'roles', 'label' => 'Rollen', 'href' => '/tenants/roles/'];
if (!empty($features['tenant_settings'])) {
$items[] = ['key' => 'settings', 'label' => 'Einstellungen', 'href' => '/settings/'];
}
if ($hasExports) {
$items[] = ['key' => 'exports', 'label' => 'Exporte', 'href' => '/exports/'];
}
}
return $items;
}
function app_platform_admin_by_email(PDO $pdo, string $email): ?array
{
return app_query_one(
+16 -23
View File
@@ -216,6 +216,7 @@ if ($auth !== null && in_array($page, $restrictedPages, true) && !app_can_manage
}
$canManageTenant = app_can_manage_tenant($auth);
$tenantNavItems = app_tenant_navigation_items($auth, $tenantLicense);
?><!DOCTYPE html>
<html lang="de">
@@ -224,8 +225,8 @@ $canManageTenant = app_can_manage_tenant($auth);
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kaffeeliste SaaS</title>
<style>
:root{--bg:#f5efe6;--card:#fffaf3;--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,.context{display:flex;flex-wrap:wrap;gap:10px}.bar{justify-content:space-between;align-items:center;padding:18px 22px;margin-bottom:18px}.brand strong,h1,h2,h3{font-family:Georgia,serif}.brand strong{font-size:1.28rem}.brand span,p,.muted{color:var(--muted)}.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))}.grid-4{grid-template-columns:repeat(4,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)}.eyebrow{display:inline-block;margin-bottom:12px;color:var(--accent);font-size:.82rem;font-weight:800;letter-spacing:.14em;text-transform:uppercase}h1{font-size:clamp(2.1rem,4vw,3.8rem);margin:0 0 12px}h2{font-size:1.4rem;margin:0 0 12px}h3{font-size:1.05rem;margin:0 0 10px}p{margin:0;line-height:1.65}.stack{display:grid;gap:12px}.metric{padding:18px;border:1px solid var(--line);border-radius:20px;background:#fffdf9}.metric strong{display:block;font-size:1.8rem;margin-bottom:8px}.list{margin:14px 0 0;padding-left:18px;color:var(--muted);line-height:1.7}.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)}.alert-info{background:rgba(12,107,102,.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,textarea{width:100%;padding:12px 14px;border-radius:16px;border:1px solid rgba(36,22,14,.15);font:inherit;background:#fffdfa;color:var(--ink)}textarea{min-height:120px}.footer{margin-top:18px;text-align:center;color:var(--muted);font-size:.92rem}@media(max-width:960px){.grid-2,.grid-3,.grid-4,form.grid{grid-template-columns:1fr}.bar{align-items:flex-start;flex-direction:column}.table table{min-width:0}}
:root{--bg:#f4efe4;--card:#fffdf8;--ink:#25180f;--muted:#63584a;--brand:#005e3f;--accent:#c18a00;--line:rgba(37,24,15,.14);--radius:18px;--shadow:0 16px 36px rgba(37,24,15,.08)}
*{box-sizing:border-box}body{margin:0;font-family:"Aptos","Segoe UI",sans-serif;color:var(--ink);background:linear-gradient(180deg,#f9f6ef 0%,var(--bg) 100%)}a{color:inherit}.shell{width:min(1180px,calc(100vw - 32px));margin:20px 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:14px}.brand strong,h1,h2,h3{font-family:Georgia,serif}.brand strong{font-size:1.18rem}.brand span,p,.muted{color:var(--muted)}.hero,.card{padding:24px}.hero{margin-bottom:18px;background:linear-gradient(180deg,#fffdf8 0%,#f9f4ea 100%)}.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))}.grid-4{grid-template-columns:repeat(4,minmax(0,1fr))}.links a,.button,button{display:inline-flex;align-items:center;justify-content:center;padding:10px 14px;border-radius:999px;text-decoration:none;font-weight:700;border:1px solid transparent;cursor:pointer}.links a{background:#fff;color:var(--brand);border-color:rgba(0,94,63,.12)}.links a.active{background:var(--brand);color:#fff}.button,button{background:var(--brand);color:#fff}.button.secondary{background:#fff;color:var(--brand);border-color:rgba(0,94,63,.18)}.eyebrow{display:inline-block;margin-bottom:12px;color:var(--accent);font-size:.82rem;font-weight:800;letter-spacing:.14em;text-transform:uppercase}h1{font-size:clamp(2rem,4vw,3.2rem);margin:0 0 12px}h2{font-size:1.35rem;margin:0 0 12px}h3{font-size:1.05rem;margin:0 0 10px}p{margin:0;line-height:1.6}.stack{display:grid;gap:12px}.metric{padding:18px;border:1px solid var(--line);border-radius:16px;background:#fff}.metric strong{display:block;font-size:1.8rem;margin-bottom:8px}.list{margin:14px 0 0;padding-left:18px;color:var(--muted);line-height:1.7}.alert{padding:16px 18px;margin-bottom:18px}.alert-success{background:rgba(0,94,63,.08)}.alert-warning{background:rgba(193,138,0,.1)}.alert-error{background:rgba(154,31,31,.1)}.alert-info{background:rgba(0,94,63,.08)}.badge{display:inline-flex;align-items:center;padding:7px 12px;border-radius:999px;font-size:.86rem;font-weight:700}.badge-neutral{background:rgba(0,94,63,.08);color:var(--brand)}.badge-success{background:rgba(0,94,63,.12);color:var(--brand)}.badge-warning{background:rgba(193,138,0,.14);color:#8c6500}.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,textarea{width:100%;padding:12px 14px;border-radius:14px;border:1px solid rgba(37,24,15,.15);font:inherit;background:#fff;color:var(--ink)}textarea{min-height:120px}.footer{margin-top:18px;text-align:center;color:var(--muted);font-size:.92rem}@media(max-width:960px){.grid-2,.grid-3,.grid-4,form.grid{grid-template-columns:1fr}.bar{align-items:flex-start;flex-direction:column}.table table{min-width:0}}
</style>
</head>
<body>
@@ -233,25 +234,17 @@ $canManageTenant = app_can_manage_tenant($auth);
<header class="bar">
<div class="brand">
<strong>Kaffeeliste SaaS</strong>
<span>Zentrale Anmeldung, Tenant-Verwaltung und alle Kernfunktionen der Kaffeeliste in einer Oberfläche.</span>
<span>Kaffeeliste, Hinweise und Support in einem Menü.</span>
</div>
<nav class="links" aria-label="Navigation">
<?php if ($auth === null): ?>
<a href="/" class="<?= $page === 'home' ? 'active' : '' ?>">Start</a>
<a href="/login/" class="<?= $page === 'login' ? 'active' : '' ?>">Anmeldung</a>
<a href="/admin/login/" class="<?= $page === 'tenants' ? 'active' : '' ?>">Für Betreiber</a>
<a href="/admin/login/" class="<?= $page === 'tenants' ? 'active' : '' ?>">Verwaltung</a>
<?php else: ?>
<a href="/dashboard/" class="<?= $page === 'dashboard' ? 'active' : '' ?>">Dashboard</a>
<a href="/content/" class="<?= $page === 'content' ? 'active' : '' ?>">Hinweise</a>
<a href="/surveys/" class="<?= $page === 'surveys' ? 'active' : '' ?>">Umfragen</a>
<a href="/support/" class="<?= $page === 'support' ? 'active' : '' ?>">Support</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>
<?php if ($hasTenantSettingsFeature): ?><a href="/settings/" class="<?= $page === 'settings' ? 'active' : '' ?>">Einstellungen</a><?php endif; ?>
<?php if ($hasPdfExportFeature || $hasPaperStrikeEntryFeature || $hasBasicExportsFeature): ?><a href="/exports/" class="<?= $page === 'exports' ? 'active' : '' ?>">Exporte</a><?php endif; ?>
<?php endif; ?>
<?php foreach ($tenantNavItems as $item): ?>
<a href="<?= h((string) ($item['href'] ?? '/')) ?>" class="<?= $page === (string) ($item['key'] ?? '') ? 'active' : '' ?>"><?= h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
<form method="post" action="/logout/"><button type="submit" class="button secondary">Abmelden</button></form>
<?php endif; ?>
</nav>
@@ -319,7 +312,7 @@ $canManageTenant = app_can_manage_tenant($auth);
<?php elseif ($page === 'login'): ?>
<section class="hero">
<div class="eyebrow">Zentrale Anmeldung</div>
<h1>Mitglieder melden sich zentral mit ihrer E-Mail-Adresse an.</h1>
<h1>Anmeldung</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 zuerst') ?>
@@ -387,7 +380,7 @@ $canManageTenant = app_can_manage_tenant($auth);
<?php elseif ($page === 'tenants'): ?>
<section class="hero">
<div class="eyebrow">Betreiber-Sicht</div>
<h1>Die Tenant-Übersicht bündelt Portfolio, Zugänge und Betriebsstatus an einem Ort.</h1>
<h1>Mandanten</h1>
<p>Hier sehen Betreiber, wie viele Tenants aktiv sind, wie die Anmeldung funktioniert und wo Mehrfachzuordnungen sauber abgefangen werden.</p>
</section>
@@ -427,7 +420,7 @@ $canManageTenant = app_can_manage_tenant($auth);
<?php elseif ($page === 'dashboard'): ?>
<section class="hero">
<div class="eyebrow">Tenant-Dashboard</div>
<h1><?= h((string) $auth['tenant_name']) ?> auf einen Blick</h1>
<h1><?= h((string) $auth['tenant_name']) ?> Übersicht</h1>
<p>Kontostand, Buchungen, Einzahlungen und die letzten Aktivitäten stehen direkt für diesen Tenant bereit.</p>
<?php if (app_is_platform_admin($auth) && app_admin_user() !== null): ?>
<div class="actions" style="margin-top:18px">
@@ -528,7 +521,7 @@ $canManageTenant = app_can_manage_tenant($auth);
<?php elseif ($page === 'members'): ?>
<section class="hero">
<div class="eyebrow">Mitgliederverwaltung</div>
<h1>Personen, Admins und Zugänge im Mandanten verwalten</h1>
<h1>Mitglieder</h1>
<p>Hier legst du neue Personen an, gibst Zugänge frei und weist bei Bedarf die Rolle als Mandanten-Admin zu.</p>
<?php if (app_is_platform_admin($auth) && app_admin_user() !== null): ?>
<div class="actions" style="margin-top:18px">
@@ -605,7 +598,7 @@ $canManageTenant = app_can_manage_tenant($auth);
</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'): ?>
<section class="hero"><div class="eyebrow">Einzahlungen</div><h1>Zahlungen tenantweit verwalten</h1><p>Einzahlungen werden direkt in Zahlungstabelle und Ledger geschrieben.</p></section>
<section class="hero"><div class="eyebrow">Einzahlungen</div><h1>Zahlungen</h1><p>Einzahlungen werden direkt in Zahlungstabelle und Ledger geschrieben.</p></section>
<section class="grid grid-2">
<article class="card">
<h2>Einzahlung buchen</h2>
@@ -622,13 +615,13 @@ $canManageTenant = app_can_manage_tenant($auth);
</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 häufige Fragen werden pro Tenant gepflegt und direkt an die Mitglieder ausgespielt.</p></section>
<section class="hero"><div class="eyebrow">Hinweise und FAQ</div><h1>Hinweise und FAQ</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 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 elseif ($page === 'settings'): ?>
<section class="hero"><div class="eyebrow">Mandanten-Einstellungen</div><h1>Einstellungen für Betrieb, Drucklisten und Kommunikation</h1><p>Diese Einstellungen gelten nur für den aktuell geöffneten Mandanten und bleiben unabhängig von anderen Bereichen.</p></section>
<section class="hero"><div class="eyebrow">Mandanten-Einstellungen</div><h1>Einstellungen</h1><p>Diese Einstellungen gelten nur für den aktuell geöffneten Mandanten und bleiben unabhängig von anderen Bereichen.</p></section>
<section class="grid grid-2">
<article class="card">
<h2>Einstellungen speichern</h2>
@@ -676,7 +669,7 @@ $canManageTenant = app_can_manage_tenant($auth);
</div>
</section>
<?php else: ?>
<section class="hero"><div class="eyebrow">Exporte und Drucklisten</div><h1>Drucklisten als PDF vorbereiten und später aus Papier nacherfassen</h1><p>Hier erzeugst du druckfertige Listen, speicherst sie als PDF und buchst anschließend die Striche aus Vorder- und Rückseite zurück ins System.</p></section>
<section class="hero"><div class="eyebrow">Exporte und Drucklisten</div><h1>Drucklisten</h1><p>Hier erzeugst du druckfertige Listen, speicherst sie als PDF und buchst anschließend die Striche aus Vorder- und Rückseite zurück ins System.</p></section>
<section class="grid grid-2">
<?php if ($hasPdfExportFeature): ?>
<article class="card">
+5
View File
@@ -7,6 +7,7 @@ if (session_status() !== PHP_SESSION_ACTIVE) {
}
require_once __DIR__ . '/../app-support.php';
require_once __DIR__ . '/../tenant-shell.php';
require_once __DIR__ . '/../../app/Modules/Support/bootstrap.php';
if (!isset($_SESSION['support_csrf'])) {
@@ -39,6 +40,7 @@ $flash = app_flash();
$pdo = null;
$dbError = null;
$supportTablesReady = false;
$tenantLicense = ['plan_name' => 'Free', 'features' => app_feature_defaults()];
$controller = new \App\Modules\Support\Controllers\SupportController(new \App\Modules\Support\Application\SupportService());
$pageData = [
'title' => 'Support',
@@ -63,6 +65,7 @@ $pageData = [
try {
$pdo = app_pdo();
$tenantLicense = app_tenant_license($pdo, (string) $auth['tenant_id']);
$supportTablesReady = scripts_table_exists($pdo, 'support_requests')
&& scripts_table_exists($pdo, 'support_request_messages');
} catch (\Throwable $exception) {
@@ -94,6 +97,8 @@ $statuses = $pageData['statuses'] ?? [];
$priorities = $pageData['priorities'] ?? [];
$routeTargets = $pageData['route_targets'] ?? [];
$selectedRequestId = (string) ($_GET['request'] ?? '');
$tenantNavItems = app_tenant_navigation_items($auth, $tenantLicense);
$tenantNavItems = app_tenant_navigation_items($auth);
$template = dirname(__DIR__, 2) . '/resources/views/support/index.blade.php';
if (!is_file($template)) {
+37 -28
View File
@@ -10,6 +10,7 @@ if (session_status() !== PHP_SESSION_ACTIVE) {
}
require_once dirname(__DIR__) . '/app-support.php';
require_once dirname(__DIR__) . '/tenant-shell.php';
require_once dirname(__DIR__, 2) . '/app/Modules/Surveys/Domain/Survey.php';
require_once dirname(__DIR__, 2) . '/app/Modules/Surveys/Domain/SurveyQuestion.php';
require_once dirname(__DIR__, 2) . '/app/Modules/Surveys/Domain/SurveyPublication.php';
@@ -32,10 +33,18 @@ if (!function_exists('dt_short')) {
}
}
$auth = app_auth_user();
$auth = app_require_auth();
$tenantId = (string) ($auth['tenant_id'] ?? 'tenant-demo');
$tenantName = (string) ($auth['tenant_name'] ?? 'Demo Tenant');
$tenantUser = (string) ($auth['display_name'] ?? 'Survey-Verantwortliche');
$tenantLicense = ['plan_name' => 'Free', 'features' => app_feature_defaults()];
try {
$pdo = app_pdo();
$tenantLicense = app_tenant_license($pdo, $tenantId);
} catch (\Throwable $ignored) {
}
$tenantNavItems = app_tenant_navigation_items($auth, $tenantLicense);
$controller = new SurveyController(new SurveyService());
$payload = $controller->index($tenantId);
@@ -48,51 +57,51 @@ $memberBoard = $data['memberBoard'];
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kaffeeliste SaaS - Surveys</title>
<title>Kaffeeliste - Umfragen</title>
<style>
:root{--bg:#f4efe6;--card:#fffaf4;--ink:#1f2933;--muted:#667085;--brand:#0f766e;--accent:#b45309;--line:rgba(31,41,51,.12);--shadow:0 24px 60px rgba(15,23,42,.08);--radius:26px;--radius-lg:20px;--content-width:1200px}
:root{--bg:#f4efe4;--card:#fffdf8;--ink:#25180f;--muted:#63584a;--brand:#005e3f;--accent:#c18a00;--line:rgba(37,24,15,.14);--shadow:0 16px 36px rgba(37,24,15,.08);--radius:18px;--radius-lg:16px;--content-width:1200px}
*{box-sizing:border-box}
body{margin:0;min-height:100vh;font-family:"Aptos","Segoe UI","Trebuchet MS",sans-serif;color:var(--ink);background:radial-gradient(circle at top left,rgba(180,83,9,.12),transparent 30%),radial-gradient(circle at top right,rgba(15,118,110,.12),transparent 26%),linear-gradient(180deg,#faf7f1 0%,var(--bg) 100%)}
body{margin:0;min-height:100vh;font-family:"Aptos","Segoe UI",sans-serif;color:var(--ink);background:linear-gradient(180deg,#f9f6ef 0%,var(--bg) 100%)}
a{color:inherit;text-decoration:none}
h1,h2,h3,.brand__title{font-family:"Palatino Linotype","Book Antiqua",Georgia,serif;letter-spacing:-.02em}
.shell{width:min(var(--content-width),calc(100vw - 32px));margin:24px auto 40px}
h1,h2,h3,.brand__title{font-family:Georgia,serif;letter-spacing:-.02em}
.shell{width:min(var(--content-width),calc(100vw - 32px));margin:20px auto 40px}
.bar,.hero,.panel,.table-card,.note{border:1px solid var(--line);border-radius:var(--radius);background:var(--card);box-shadow:var(--shadow)}
.bar{display:flex;justify-content:space-between;gap:16px;align-items:center;padding:18px 22px;margin-bottom:18px}
.brand{display:grid;gap:4px}
.brand__title{font-size:1.25rem;font-weight:700}
.brand__title{font-size:1.18rem;font-weight:700}
.brand__subtitle{color:var(--muted)}
.toolbar,.meta,.stack{display:flex;flex-wrap:wrap;gap:10px}
.badge,.pill{display:inline-flex;align-items:center;padding:7px 12px;border-radius:999px;background:rgba(15,118,110,.08);color:var(--brand);font-weight:700;font-size:.86rem}
.badge--solid{background:linear-gradient(135deg,var(--brand),#0f5752);color:#fff}
.hero{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(260px,.65fr);gap:20px;padding:28px;margin-bottom:18px}
.badge,.pill{display:inline-flex;align-items:center;padding:7px 12px;border-radius:999px;background:#fff;color:var(--brand);font-weight:700;font-size:.86rem;border:1px solid rgba(0,94,63,.12)}
.badge--solid{background:var(--brand);color:#fff}
.hero{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(260px,.65fr);gap:20px;padding:24px;margin-bottom:18px;background:linear-gradient(180deg,#fffdf8 0%,#f9f4ea 100%)}
.hero__kicker{margin:0 0 10px;color:var(--accent);text-transform:uppercase;letter-spacing:.15em;font-size:.8rem;font-weight:800}
.hero__title{margin:0 0 12px;font-size:clamp(2rem,4vw,3.5rem);line-height:1.05}
.hero__title{margin:0 0 12px;font-size:clamp(1.9rem,4vw,3rem);line-height:1.05}
.hero__lead{margin:0;color:var(--muted);max-width:70ch;line-height:1.7}
.hero__actions{display:flex;flex-wrap:wrap;gap:12px;margin-top:16px}
.button{display:inline-flex;align-items:center;justify-content:center;padding:11px 16px;border-radius:999px;border:0;background:linear-gradient(135deg,var(--brand),#0f5752);color:#fff;font-weight:700}
.button--ghost{background:transparent;color:var(--brand);border:1px solid rgba(15,118,110,.18)}
.button{display:inline-flex;align-items:center;justify-content:center;padding:10px 14px;border-radius:999px;border:1px solid transparent;background:var(--brand);color:#fff;font-weight:700}
.button--ghost{background:#fff;color:var(--brand);border:1px solid rgba(0,94,63,.18)}
.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))}
.card,.panel,.table-card{padding:22px}
.metric{padding:18px;border:1px solid var(--line);border-radius:20px;background:#fffdf8}
.metric{padding:18px;border:1px solid var(--line);border-radius:16px;background:#fff}
.metric__label,.muted{color:var(--muted)}
.metric__value{font-size:1.8rem;font-weight:800;margin:6px 0 8px}
.table-card__header{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;margin-bottom:16px}
table{width:100%;border-collapse:collapse}
th,td{padding:12px 10px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}
th{font-size:.82rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}
.status{display:inline-flex;align-items:center;padding:6px 10px;border-radius:999px;background:rgba(15,118,110,.1);color:var(--brand);font-weight:700;font-size:.84rem}
.status--warning{background:rgba(180,83,9,.12);color:#a34b12}
.status--success{background:rgba(17,98,61,.12);color:#11623d}
.status{display:inline-flex;align-items:center;padding:6px 10px;border-radius:999px;background:rgba(0,94,63,.1);color:var(--brand);font-weight:700;font-size:.84rem}
.status--warning{background:rgba(193,138,0,.14);color:#8c6500}
.status--success{background:rgba(0,94,63,.12);color:var(--brand)}
.feature-list__item{display:flex;gap:12px;padding:14px 16px;border-radius:16px;background:rgba(255,255,255,.78);border:1px solid rgba(31,41,51,.08)}
.feature-list__badge{flex:0 0 auto;width:34px;height:34px;border-radius:12px;display:grid;place-items:center;background:rgba(15,118,110,.1);color:var(--brand);font-weight:800}
.feature-list__badge{flex:0 0 auto;width:34px;height:34px;border-radius:12px;display:grid;place-items:center;background:rgba(0,94,63,.1);color:var(--brand);font-weight:800}
.feature-list__title{margin:0 0 4px;font-weight:700}
.timeline{display:grid;gap:12px}
.timeline__item{padding:14px 16px;border-radius:16px;background:rgba(255,255,255,.72);border:1px solid rgba(31,41,51,.08)}
.timeline__item{padding:14px 16px;border-radius:16px;background:#fff;border:1px solid rgba(31,41,51,.08)}
.timeline__title{margin:0 0 4px;font-weight:700}
.timeline__meta{margin:0;color:var(--muted)}
.note{padding:16px 18px;margin-top:16px;background:rgba(15,118,110,.06)}
.note{padding:16px 18px;margin-top:16px;background:rgba(0,94,63,.06)}
.list-reset{margin:0;padding-left:18px}
.list-reset li+li{margin-top:10px}
@media(max-width:960px){.hero,.grid--2,.grid--3{grid-template-columns:1fr}.bar{align-items:flex-start;flex-direction:column}}
@@ -103,22 +112,22 @@ $memberBoard = $data['memberBoard'];
<header class="bar">
<div class="brand">
<strong class="brand__title">Kaffeeliste SaaS</strong>
<span class="brand__subtitle">Surveys als tenantfaehiger Verwaltungsbereich mit Snapshot-Publishing.</span>
<span class="brand__subtitle">Umfragen und Freigaben im Tenant.</span>
</div>
<div class="toolbar">
<span class="badge"><?= h($tenantName) ?></span>
<span class="badge">Tenant-Admin / Survey-Manager</span>
<span class="badge badge--solid">Snapshot-Modus</span>
<?php foreach ($tenantNavItems as $item): ?>
<a class="badge <?= (($item['key'] ?? '') === 'surveys') ? 'badge--solid' : '' ?>" href="<?= h((string) ($item['href'] ?? '/')) ?>"><?= h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
<form method="post" action="/logout/"><button type="submit" class="button button--ghost">Abmelden</button></form>
</div>
</header>
<section class="hero">
<div>
<p class="hero__kicker">Survey Admin</p>
<h1 class="hero__title">Umfragen werden fachlich gepflegt und als veroeffentlichte Version ausgeliefert.</h1>
<p class="hero__kicker">Umfragen</p>
<h1 class="hero__title">Umfragen</h1>
<p class="hero__lead">
Entwuerfe bleiben im Admin-Bereich bearbeitbar. Erst nach Freigabe wird ein Snapshot erzeugt,
den Mitglieder lesen koennen. So bleibt die Fachseite flexibel und die Mitgliederansicht stabil.
Entwürfe bleiben intern bearbeitbar. Mitglieder sehen nur veröffentlichte Stände.
</p>
<div class="hero__actions">
<a class="button" href="#admin-surveys">Admin Uebersicht</a>
+323
View File
@@ -0,0 +1,323 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/app-support.php';
function tenant_shell_h(string $value): string
{
return app_h($value);
}
function tenant_shell_styles(): string
{
return <<<'HTML'
<style>
:root{
--bg:#f4efe4;
--card:#fffdf8;
--card-soft:#f7f1e5;
--ink:#25180f;
--muted:#63584a;
--brand:#005e3f;
--brand-strong:#00452f;
--accent:#c18a00;
--line:rgba(37,24,15,.14);
--shadow:0 16px 36px rgba(37,24,15,.08);
--radius:18px;
}
*{box-sizing:border-box}
body{
margin:0;
color:var(--ink);
font-family:"Aptos","Segoe UI",sans-serif;
background:linear-gradient(180deg,#f9f6ef 0%,var(--bg) 100%);
}
a{color:inherit;text-decoration:none}
.tenant-shell{width:min(1240px,calc(100vw - 32px));margin:20px auto 40px}
.tenant-header,
.tenant-nav,
.tenant-context,
.hero,
.card,
.panel,
.table-card,
.note,
.alert{
border:1px solid var(--line);
border-radius:var(--radius);
background:var(--card);
box-shadow:var(--shadow);
}
.tenant-header{
display:flex;
justify-content:space-between;
align-items:flex-start;
gap:16px;
padding:18px 22px;
margin-bottom:14px;
}
.tenant-brand{display:grid;gap:4px}
.tenant-brand__title,
.hero__title,
.card h2,
.card h3,
.panel h2,
.panel h3,
.table-card h2,
.table-card h3{
margin:0;
font-family:Georgia,serif;
letter-spacing:-.02em;
}
.tenant-brand__title{font-size:1.18rem}
.tenant-brand__subtitle,
.muted,
p{color:var(--muted)}
.tenant-toolbar,
.tenant-nav__items,
.hero__actions,
.hero__meta,
.actions,
.context,
.stack-inline{
display:flex;
flex-wrap:wrap;
gap:10px;
align-items:center;
}
.tenant-nav{
display:flex;
flex-wrap:wrap;
gap:14px;
align-items:center;
padding:14px 18px;
margin-bottom:14px;
background:var(--card-soft);
}
.tenant-nav__label{
font-size:.86rem;
font-weight:800;
letter-spacing:.12em;
text-transform:uppercase;
color:var(--accent);
}
.tenant-nav__items{gap:8px}
.tenant-nav__link,
.button,
button{
display:inline-flex;
align-items:center;
justify-content:center;
padding:10px 14px;
border-radius:999px;
border:1px solid transparent;
font:inherit;
font-weight:700;
cursor:pointer;
}
.tenant-nav__link{
background:#fff;
color:var(--brand-strong);
border-color:rgba(0,94,63,.12);
}
.tenant-nav__link.is-active,
.button,
button{
background:var(--brand);
color:#fff;
}
.button.secondary{
background:#fff;
color:var(--brand-strong);
border-color:rgba(0,94,63,.18);
}
.tenant-context{
display:flex;
flex-wrap:wrap;
gap:10px;
align-items:center;
padding:14px 18px;
margin-bottom:18px;
}
.badge,
.pill{
display:inline-flex;
align-items:center;
gap:8px;
padding:6px 11px;
border-radius:999px;
border:1px solid rgba(0,94,63,.12);
background:#fff;
color:var(--brand-strong);
font-size:.84rem;
font-weight:700;
}
.badge--solid,
.badge--success{background:rgba(0,94,63,.1);color:var(--brand-strong)}
.badge--warning{background:rgba(193,138,0,.14);color:#8c6500}
.badge--danger{background:rgba(154,31,31,.12);color:#9a1f1f}
.badge--neutral{background:rgba(0,94,63,.06);color:var(--brand-strong)}
.hero{
display:grid;
gap:18px;
padding:24px;
margin-bottom:18px;
background:linear-gradient(180deg,#fffdf8 0%,#f9f4ea 100%);
}
.hero--split{grid-template-columns:minmax(0,1.2fr) minmax(280px,.8fr)}
.hero__content,
.hero__aside,
.stack{display:grid;gap:14px}
.hero__kicker,
.card__eyebrow,
.eyebrow{
margin:0 0 8px;
font-size:.8rem;
font-weight:800;
text-transform:uppercase;
letter-spacing:.12em;
color:var(--accent);
}
.hero__title{font-size:clamp(1.9rem,4vw,3rem);line-height:1.05}
.hero__lead{margin:0;max-width:64ch;line-height:1.6}
.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))}
.grid--4{grid-template-columns:repeat(4,minmax(0,1fr))}
.split{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(360px,.9fr);gap:18px}
.card,.panel,.table-card{padding:22px}
.table-card__header{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;margin-bottom:16px;flex-wrap:wrap}
.table{overflow-x:auto}
table{width:100%;border-collapse:collapse;min-width:720px}
th,td{padding:12px 10px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}
th{font-size:.82rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
input,select,textarea{
width:100%;
padding:12px 14px;
border-radius:14px;
border:1px solid rgba(37,24,15,.15);
background:#fff;
color:var(--ink);
font:inherit;
}
label{display:flex;flex-direction:column;gap:8px;font-weight:700}
textarea{min-height:120px}
.metric{
padding:18px;
border-radius:16px;
border:1px solid var(--line);
background:#fff;
display:grid;
gap:8px;
}
.metric__label{color:var(--muted)}
.metric__value{font-size:1.9rem;font-weight:800}
.status{
display:inline-flex;
align-items:center;
padding:6px 10px;
border-radius:999px;
background:rgba(0,94,63,.1);
color:var(--brand-strong);
font-weight:700;
font-size:.84rem;
}
.status--warning{background:rgba(193,138,0,.14);color:#8c6500}
.status--success{background:rgba(0,94,63,.12);color:var(--brand-strong)}
.status--danger{background:rgba(154,31,31,.12);color:#9a1f1f}
.note,
.alert{
padding:14px 16px;
}
.alert-success{background:rgba(0,94,63,.08)}
.alert-warning{background:rgba(193,138,0,.1)}
.alert-error{background:rgba(154,31,31,.1)}
.timeline{display:grid;gap:12px}
.timeline__item{
padding:14px 16px;
border-radius:16px;
border:1px solid rgba(37,24,15,.08);
background:#fff;
}
.timeline__title{margin:0 0 6px;font-weight:800;color:var(--ink)}
.timeline__meta{margin:0;color:var(--muted);line-height:1.55}
.tenant-footer{
margin-top:18px;
text-align:center;
color:var(--muted);
font-size:.92rem;
}
@media(max-width:980px){
.tenant-header,
.tenant-nav{flex-direction:column;align-items:flex-start}
.hero--split,
.grid--2,
.grid--3,
.grid--4,
.split{grid-template-columns:1fr}
table{min-width:0}
}
</style>
HTML;
}
function tenant_shell_path_is_active(string $currentPath, string $href): bool
{
$normalize = static function (string $path): string {
$value = parse_url($path, PHP_URL_PATH);
$value = is_string($value) ? rtrim($value, '/') : '';
return $value === '' ? '/' : $value;
};
return $normalize($currentPath) === $normalize($href);
}
function tenant_shell_render_header(array $auth, string $currentPath, array $tenantLicense = [], string $title = 'Kaffeeliste', string $subtitle = 'Mandantenbereich'): string
{
$navItems = app_tenant_navigation_items($auth, $tenantLicense);
$tenantName = (string) ($auth['tenant_name'] ?? 'Tenant');
$displayName = (string) ($auth['display_name'] ?? 'Benutzer');
$roleLabel = app_primary_role_label($auth);
$planName = (string) ($tenantLicense['plan_name'] ?? 'Free');
ob_start();
?>
<header class="tenant-header">
<div class="tenant-brand">
<h1 class="tenant-brand__title"><?= tenant_shell_h($title) ?></h1>
<p class="tenant-brand__subtitle"><?= tenant_shell_h($subtitle) ?></p>
</div>
<div class="tenant-toolbar">
<span class="badge"><?= tenant_shell_h($tenantName) ?></span>
<span class="badge"><?= tenant_shell_h($roleLabel) ?></span>
<span class="badge"><?= tenant_shell_h('Lizenz ' . $planName) ?></span>
</div>
</header>
<nav class="tenant-nav" aria-label="Tenant-Menue">
<span class="tenant-nav__label">Menue</span>
<div class="tenant-nav__items">
<?php foreach ($navItems as $item): ?>
<a href="<?= tenant_shell_h($item['href']) ?>" class="tenant-nav__link<?= tenant_shell_path_is_active($currentPath, (string) $item['href']) ? ' is-active' : '' ?>">
<?= tenant_shell_h((string) $item['label']) ?>
</a>
<?php endforeach; ?>
<form method="post" action="/logout/" style="display:inline-flex;">
<button type="submit" class="button secondary">Abmelden</button>
</form>
</div>
</nav>
<section class="tenant-context">
<span class="badge"><?= tenant_shell_h($displayName) ?></span>
<?php foreach (app_auth_role_labels($auth) as $label): ?>
<span class="badge badge--neutral"><?= tenant_shell_h($label) ?></span>
<?php endforeach; ?>
<?php if (!empty($auth['acting_as_platform_admin'])): ?>
<span class="badge badge--warning">Global-Admin im Tenant</span>
<?php endif; ?>
</section>
<?php
return (string) ob_get_clean();
}
+10
View File
@@ -7,6 +7,7 @@ if (session_status() !== PHP_SESSION_ACTIVE) {
}
require_once __DIR__ . '/../../app-support.php';
require_once __DIR__ . '/../../tenant-shell.php';
require_once dirname(__DIR__, 3) . '/app/Support/TenantResolver.php';
require_once dirname(__DIR__, 3) . '/app/Modules/Tenants/Models/Tenant.php';
require_once dirname(__DIR__, 3) . '/app/Modules/Tenants/Models/TenantUser.php';
@@ -33,6 +34,7 @@ if (!function_exists('tenant_roles_implode')) {
}
$auth = app_require_auth();
$tenantLicense = ['plan_name' => 'Free', 'features' => app_feature_defaults()];
if (!app_can_manage_tenant($auth)) {
app_flash('Dieser Bereich ist nur für Tenant-Admins oder Global-Admins verfügbar.', 'warning');
@@ -46,6 +48,12 @@ $roleService = new \App\Modules\Tenants\Services\TenantRoleService(
);
$controller = new \App\Modules\Tenants\Controllers\TenantConsoleController($tenantService, $roleService);
try {
$pdo = app_pdo();
$tenantLicense = app_tenant_license($pdo, (string) ($auth['tenant_id'] ?? ''));
} catch (\Throwable $ignored) {
}
$tenantId = (string) ($_GET['tenant'] ?? $auth['tenant_id'] ?? '');
$payload = $controller->roles($tenantId !== '' ? $tenantId : null);
$pageData = $payload['data'] ?? [];
@@ -57,6 +65,8 @@ $delegationRules = $overview['delegation_rules'] ?? [];
$tenantSnapshots = $overview['tenant_snapshots'] ?? [];
$permissionGroups = $overview['permission_groups'] ?? [];
$notes = $overview['notes'] ?? [];
$tenantNavItems = app_tenant_navigation_items($auth, $tenantLicense);
$tenantNavItems = app_tenant_navigation_items($auth);
$template = dirname(__DIR__, 3) . '/resources/views/tenants/roles.blade.php';
if (!is_file($template)) {
@@ -1,6 +1,6 @@
@extends('layouts.app')
@section('page_title', 'Kaffeeliste SaaS - Content Redaktion')
@section('page_title', 'Kaffeeliste SaaS - Redaktion')
@php
$editorial = $editorial ?? [
@@ -17,8 +17,8 @@
<section class="hero hero--split">
<div class="hero__content">
<div>
<p class="hero__kicker">Tenant-Admin-Perspektive</p>
<h2 class="hero__title">Content-Redaktion mit Standardvorlage und Tenant-Override.</h2>
<p class="hero__kicker">Redaktion</p>
<h2 class="hero__title">Redaktion</h2>
<p class="hero__lead">
Diese Sicht ist fuer Tenant-Admins gedacht: Hinweise, FAQ und spaetere Textbausteine
folgen einer klaren Redaktionslogik mit Freigabe, Sichtbarkeit und tenantbezogener Pflege.
@@ -1,6 +1,6 @@
@extends('layouts.app')
@section('page_title', 'Kaffeeliste SaaS - Content')
@section('page_title', 'Kaffeeliste SaaS - Hinweise')
@php
$data = $content ?? [
@@ -25,8 +25,8 @@
<section class="hero hero--split">
<div class="hero__content">
<div>
<p class="hero__kicker">Content-MVP</p>
<h2 class="hero__title">Hinweise und FAQ werden zu tenantfaehigen Redaktionsobjekten.</h2>
<p class="hero__kicker">Hinweise</p>
<h2 class="hero__title">Hinweise und FAQ</h2>
<p class="hero__lead">
Das Content-Modul zeigt nicht mehr nur Demo-Kacheln, sondern die fachliche
Struktur fuer freigabefaehige Hinweise, tenantbezogene FAQ und eine klare
@@ -1,12 +1,12 @@
@extends('layouts.app')
@section('page_title', 'Kaffeeliste SaaS - Dashboard')
@section('page_title', 'Kaffeeliste SaaS - Uebersicht')
@section('content')
<section class="hero">
<div>
<p class="hero__kicker">Tenant overview</p>
<h2 class="hero__title">Dein Kaffeeliste-Stand auf einen Blick.</h2>
<p class="hero__kicker">Uebersicht</p>
<h2 class="hero__title">Dein Bereich</h2>
<p class="hero__lead">
Das Dashboard zeigt den aktuellen Kontostand, die Nutzung im Monat und die letzten Buchungen.
Die Werte sind hier bewusst als fachliche Anker platziert und koennen spaeter direkt aus dem Ledger gespeist werden.
@@ -1,12 +1,12 @@
@extends('layouts.app')
@section('page_title', 'Kaffeeliste SaaS - Exports')
@section('page_title', 'Kaffeeliste SaaS - Exporte')
@section('content')
<section class="hero">
<div>
<p class="hero__kicker">Exports</p>
<h2 class="hero__title">Reports und Drucklisten bleiben moeglich, aber nicht mehr als Spezialskript.</h2>
<p class="hero__kicker">Exporte</p>
<h2 class="hero__title">Exporte und Drucklisten</h2>
<p class="hero__lead">
Das fruehere PDF fuer die Papierliste wandert in ein Export-Modul. Neben
Drucklisten entstehen hier Reports fuer Finance, Mitglieder und
@@ -1,12 +1,12 @@
@extends('layouts.app')
@section('page_title', 'Kaffeeliste SaaS - Imports')
@section('page_title', 'Kaffeeliste SaaS - Importe')
@section('content')
<section class="hero">
<div>
<p class="hero__kicker">Imports</p>
<h2 class="hero__title">Dateiimporte werden kontrollierte Jobs statt einmaliger Root-Skripte.</h2>
<p class="hero__kicker">Importe</p>
<h2 class="hero__title">Importe</h2>
<p class="hero__lead">
CSV-Uploads und Legacy-Datenuebernahmen bleiben moeglich, laufen aber als
nachvollziehbare Importjobs mit Vorschau, Mapping und Statusmeldungen.
+58 -46
View File
@@ -5,20 +5,29 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>@yield('page_title', $title ?? 'Kaffeeliste SaaS')</title>
@php
$layoutAuth = $auth ?? (function_exists('app_auth_user') ? app_auth_user() : null);
$layoutLicense = $tenantLicense ?? ['features' => []];
$layoutPath = parse_url((string) ($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH);
$layoutPath = is_string($layoutPath) ? rtrim($layoutPath, '/') : '/';
$layoutNavItems = function_exists('app_tenant_navigation_items')
? app_tenant_navigation_items($layoutAuth, $layoutLicense)
: [];
@endphp
<style>
:root {
--bg: #f4efe6;
--bg-elevated: rgba(255, 255, 255, 0.86);
--bg-soft: rgba(249, 242, 231, 0.9);
--text: #1f2933;
--muted: #667085;
--brand: #0f766e;
--brand-strong: #134e4a;
--accent: #b45309;
--line: rgba(31, 41, 51, 0.12);
--shadow: 0 24px 60px rgba(15, 23, 42, 0.08);
--radius-xl: 28px;
--radius-lg: 20px;
--bg: #efe6d6;
--bg-elevated: rgba(255, 251, 244, 0.96);
--bg-soft: rgba(247, 239, 227, 0.96);
--text: #2c2017;
--muted: #6d5d4f;
--brand: #2d6a4f;
--brand-strong: #234f3c;
--accent: #9f5a1d;
--line: rgba(44, 32, 23, 0.14);
--shadow: 0 18px 42px rgba(68, 48, 34, 0.08);
--radius-xl: 24px;
--radius-lg: 18px;
--content-width: 1220px;
}
@@ -29,10 +38,8 @@
min-height: 100vh;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(180, 83, 9, 0.12), transparent 32%),
radial-gradient(circle at top right, rgba(15, 118, 110, 0.14), transparent 28%),
linear-gradient(180deg, #faf7f1 0%, var(--bg) 100%);
font-family: "Aptos", "Segoe UI", "Trebuchet MS", sans-serif;
linear-gradient(180deg, #f8f1e6 0%, var(--bg) 100%);
font-family: "Trebuchet MS", "Aptos", "Segoe UI", sans-serif;
line-height: 1.55;
}
@@ -51,8 +58,8 @@
top: 0;
z-index: 50;
border-bottom: 1px solid var(--line);
background: rgba(251, 248, 242, 0.88);
backdrop-filter: blur(18px);
background: rgba(249, 242, 231, 0.96);
backdrop-filter: blur(12px);
}
.app-header__inner,
.app-main,
@@ -77,7 +84,7 @@
font-weight: 700;
color: #fff;
background: linear-gradient(135deg, var(--brand) 0%, #115e59 55%, var(--accent) 100%);
box-shadow: 0 18px 40px rgba(15, 118, 110, 0.25);
box-shadow: 0 12px 26px rgba(45, 106, 79, 0.2);
}
.brand__text { display: grid; gap: 2px; min-width: 0; }
.brand__title { margin: 0; font-size: 1.02rem; font-weight: 700; letter-spacing: 0.01em; }
@@ -113,20 +120,20 @@
.app-nav a {
padding: 0.65rem 0.95rem;
border-radius: 999px;
border: 1px solid transparent;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(44, 32, 23, 0.08);
background: rgba(255, 251, 244, 0.88);
color: var(--text);
font-size: 0.92rem;
font-weight: 600;
}
.app-nav a.is-primary {
background: linear-gradient(135deg, var(--brand) 0%, #115e59 100%);
background: linear-gradient(135deg, var(--brand) 0%, #356f56 100%);
color: #fff;
}
.app-nav a:hover {
text-decoration: none;
border-color: rgba(15, 118, 110, 0.2);
background: #fff;
border-color: rgba(45, 106, 79, 0.2);
background: #fffdf8;
}
.app-main { padding: 34px 0 56px; }
.hero {
@@ -137,9 +144,7 @@
border: 1px solid var(--line);
border-radius: var(--radius-xl);
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0.8)),
radial-gradient(circle at top right, rgba(180, 83, 9, 0.15), transparent 28%),
radial-gradient(circle at bottom left, rgba(15, 118, 110, 0.16), transparent 26%);
linear-gradient(135deg, rgba(255, 251, 244, 0.98), rgba(252, 247, 240, 0.95));
box-shadow: var(--shadow);
}
.hero--split {
@@ -208,7 +213,6 @@
border: 1px solid var(--line);
border-radius: var(--radius-lg);
background: var(--bg-elevated);
backdrop-filter: blur(10px);
box-shadow: var(--shadow);
padding: 22px;
}
@@ -305,8 +309,8 @@
.callout {
padding: 16px 18px;
border-radius: 18px;
background: rgba(15, 118, 110, 0.08);
border: 1px solid rgba(15, 118, 110, 0.12);
background: rgba(45, 106, 79, 0.08);
border: 1px solid rgba(45, 106, 79, 0.14);
color: var(--brand-strong);
}
.callout strong {
@@ -436,29 +440,37 @@
<div class="app-header__inner">
<div class="brand">
<div class="brand__mark">K</div>
<div class="brand__text">
<div class="brand__text">
<h1 class="brand__title">Kaffeeliste SaaS</h1>
<p class="brand__subtitle">Mandantenfaehige Buchungen, Mitglieder und Auswertungen</p>
<p class="brand__subtitle">Kaffeekasse, Mitglieder und Verwaltung pro Tenant</p>
</div>
</div>
<div class="header-meta">
<span class="badge">Webspace-tauglich</span>
<span class="badge badge--solid">Mandantenfaehig</span>
@if (is_array($layoutAuth))
<span class="badge">{{ $layoutAuth['tenant_name'] ?? 'Tenant' }}</span>
@if (function_exists('app_can_manage_tenant') && app_can_manage_tenant($layoutAuth))
<span class="badge badge--solid">Verwaltung</span>
@else
<span class="badge badge--solid">Mitglied</span>
@endif
@else
<span class="badge">Klassische Oberflaeche</span>
<span class="badge badge--solid">Mandantenfaehig</span>
@endif
</div>
</div>
<nav class="app-nav" aria-label="Hauptnavigation">
<a class="is-primary" href="/">Start</a>
<a href="/login">Anmeldung</a>
<a href="/tenants">Mandanten-Admin</a>
<a href="/dashboard">Dashboard</a>
<a href="/members">Mitglieder</a>
<a href="/ledger">Ledger</a>
<a href="/payments">Payments</a>
<a href="/content">Content</a>
<a href="/imports">Imports</a>
<a href="/exports">Exports</a>
<a href="/notifications">Notifications</a>
<a href="/surveys">Surveys</a>
@forelse ($layoutNavItems as $item)
@php
$itemHref = rtrim((string) ($item['href'] ?? '/'), '/');
$itemHref = $itemHref === '' ? '/' : $itemHref;
$isActive = $layoutPath === $itemHref;
@endphp
<a href="{{ $item['href'] ?? '/' }}" class="{{ $isActive ? 'is-primary' : '' }}">{{ $item['label'] ?? 'Link' }}</a>
@empty
<a class="is-primary" href="/">Start</a>
<a href="/login/">Anmeldung</a>
@endforelse
</nav>
</header>
@@ -1,6 +1,6 @@
@extends('layouts.app')
@section('page_title', 'Kaffeeliste SaaS - Ledger')
@section('page_title', 'Kaffeeliste SaaS - Buchungen')
@php
$overview = $ledgerOverview ?? [];
@@ -30,8 +30,8 @@
<section class="hero hero--split">
<div class="hero__content">
<div>
<p class="hero__kicker">Accounting flow</p>
<h2 class="hero__title">Ledger fuer Buchungen, Verbrauch, Storno und loeschbare Striche.</h2>
<p class="hero__kicker">Buchungen</p>
<h2 class="hero__title">Buchungen</h2>
<p class="hero__lead">
Das Ledger bildet die fachliche Buchungsspur der Kaffeeliste ab. Einzahlungen sind als Storno
sichtbar, Striche bleiben als verantwortbare Loeschung erkennbar und Korrekturen bleiben auditierbar.
@@ -5,8 +5,8 @@
@section('content')
<section class="hero">
<div>
<p class="hero__kicker">Member management</p>
<h2 class="hero__title">Mitgliederverwaltung pro Mandant.</h2>
<p class="hero__kicker">Mitglieder</p>
<h2 class="hero__title">Mitglieder</h2>
<p class="hero__lead">
Hier sind aktive Mitglieder, Rollen und Status klar gegliedert. Diese Sicht entspricht der alten
Mitgliederverwaltung, nur als SaaS-taugliche, aufgeraeumte Oberflaeche.
@@ -1,12 +1,12 @@
@extends('layouts.app')
@section('page_title', 'Kaffeeliste SaaS - Notifications')
@section('page_title', 'Kaffeeliste SaaS - Benachrichtigungen')
@section('content')
<section class="hero">
<div>
<p class="hero__kicker">Notifications</p>
<h2 class="hero__title">Benachrichtigungen werden planbare Betriebsprozesse.</h2>
<p class="hero__kicker">Benachrichtigungen</p>
<h2 class="hero__title">Benachrichtigungen</h2>
<p class="hero__lead">
Die alte Sammelmail-Funktion geht in ein Modul ueber, das Versandregeln,
Cron-Ausfuehrung und Ergebnisprotokolle sauber trennt. Damit werden
@@ -1,6 +1,6 @@
@extends('layouts.app')
@section('page_title', 'Kaffeeliste SaaS - Payments')
@section('page_title', 'Kaffeeliste SaaS - Zahlungen')
@php
$overview = $paymentOverview ?? [];
@@ -24,8 +24,8 @@
<section class="hero hero--split">
<div class="hero__content">
<div>
<p class="hero__kicker">Payments</p>
<h2 class="hero__title">Einzahlungen werden tenantfaehig verwaltet und fuer spaetere Abgleiche vorbereitet.</h2>
<p class="hero__kicker">Zahlungen</p>
<h2 class="hero__title">Zahlungen</h2>
<p class="hero__lead">
Das Payments-Modul bildet die fachlich gewollte Konfiguration fuer Bar, Ueberweisung und PayPal ab.
Reconciliation bleibt bewusst als Folgepaket vorbereitet, damit der MVP bei manuellen Buchungen stabil bleibt.
@@ -6,46 +6,43 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kaffeeliste SaaS - Support</title>
<title>Kaffeeliste - Support</title>
<style>
:root{
--bg:#f5efe6;
--card:#fffaf3;
--ink:#24160e;
--muted:#6a5848;
--brand:#0c6b66;
--brand-2:#084d49;
--accent:#a34b12;
--line:rgba(36,22,14,.12);
--shadow:0 24px 54px rgba(57,35,22,.11);
--radius:24px;
--bg:#f4efe4;
--card:#fffdf8;
--ink:#25180f;
--muted:#63584a;
--brand:#005e3f;
--brand-2:#00452f;
--accent:#c18a00;
--line:rgba(37,24,15,.14);
--shadow:0 16px 36px rgba(37,24,15,.08);
--radius:18px;
}
*{box-sizing:border-box}
body{
margin:0;
color:var(--ink);
font-family:"Aptos","Segoe UI",sans-serif;
background:
radial-gradient(circle at top left, rgba(163,75,18,.10), transparent 26%),
radial-gradient(circle at top right, rgba(12,107,102,.14), transparent 24%),
linear-gradient(180deg,#fbf8f2 0%,var(--bg) 100%);
background:linear-gradient(180deg,#f9f6ef 0%,var(--bg) 100%);
}
a{color:inherit}
.shell{width:min(1280px,calc(100vw - 32px));margin:24px auto 40px}
.shell{width:min(1240px,calc(100vw - 32px));margin:20px auto 40px}
.bar,.hero,.card,.alert{border:1px solid var(--line);border-radius:var(--radius);background:var(--card);box-shadow:var(--shadow)}
.bar{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:18px 22px;margin-bottom:18px;flex-wrap:wrap}
.brand strong,.hero h1,.card h2,.card h3{font-family:Georgia,serif}
.brand strong{font-size:1.28rem}
.brand strong{font-size:1.18rem}
.brand span,p,.muted{color:var(--muted)}
.links,.actions,.context{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.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),var(--brand-2));color:#fff}
.button,button{background:linear-gradient(135deg,var(--brand),var(--brand-2));color:#fff}
.button.secondary{background:transparent;color:var(--brand);border:1px solid rgba(12,107,102,.18)}
.hero{padding:28px;margin-bottom:18px;display:grid;gap:18px}
.links a,.button,button{display:inline-flex;align-items:center;justify-content:center;padding:10px 14px;border-radius:999px;text-decoration:none;font-weight:700;border:1px solid transparent;cursor:pointer}
.links a{background:#fff;color:var(--brand);border-color:rgba(0,94,63,.12)}
.links a.active{background:var(--brand);color:#fff}
.button,button{background:var(--brand);color:#fff}
.button.secondary{background:#fff;color:var(--brand);border-color:rgba(0,94,63,.18)}
.hero{padding:24px;margin-bottom:18px;display:grid;gap:18px;background:linear-gradient(180deg,#fffdf8 0%,#f9f4ea 100%)}
.hero__kicker{text-transform:uppercase;letter-spacing:.16em;color:var(--accent);font-size:.8rem;font-weight:800;margin:0}
.hero__title{margin:0;font-size:clamp(2rem,4vw,3.6rem);line-height:1.05}
.hero__title{margin:0;font-size:clamp(1.9rem,4vw,3rem);line-height:1.05}
.hero__lead{margin:0;max-width:72ch;font-size:1.02rem;line-height:1.65}
.grid{display:grid;gap:18px}
.grid--2{grid-template-columns:repeat(2,minmax(0,1fr))}
@@ -54,7 +51,7 @@
.card{padding:22px}
.card h2,.card h3{margin:0 0 12px}
.eyebrow{display:inline-block;margin-bottom:10px;color:var(--accent);font-size:.82rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase}
.metric{padding:18px;border-radius:20px;border:1px solid var(--line);background:#fffdf9;display:grid;gap:8px}
.metric{padding:18px;border-radius:16px;border:1px solid var(--line);background:#fff;display:grid;gap:8px}
.metric strong{font-size:1.8rem}
.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)}
@@ -75,7 +72,7 @@
input,select,textarea{width:100%;padding:12px 14px;border-radius:16px;border:1px solid rgba(36,22,14,.15);font:inherit;background:#fffdfa;color:var(--ink)}
textarea{min-height:120px}
.timeline{display:grid;gap:12px}
.timeline__item{padding:14px 16px;border-radius:18px;background:#fffdf9;border:1px solid rgba(31,41,51,.08)}
.timeline__item{padding:14px 16px;border-radius:16px;background:#fff;border:1px solid rgba(31,41,51,.08)}
.timeline__meta{margin:0;color:var(--muted);font-size:.94rem;line-height:1.6}
.timeline__title{margin:0 0 6px;font-weight:800}
.pill{display:inline-flex;align-items:center;padding:7px 12px;border-radius:999px;border:1px solid rgba(12,107,102,.15);background:rgba(12,107,102,.07);color:var(--brand);font-size:.84rem;font-weight:700}
@@ -97,28 +94,27 @@
<main class="shell">
<header class="bar">
<div class="brand">
<strong>Kaffeeliste Support</strong>
<span>Vollstaendiges Vorgangssystem fuer Tenant-Anfragen und zentrale Rueckmeldungen.</span>
<strong>Kaffeeliste SaaS</strong>
<span>Support, Vorgaenge und Rueckmeldungen im Tenant.</span>
</div>
<nav class="links">
<a href="/dashboard/">Dashboard</a>
<a href="/content/">Hinweise</a>
<a href="/support/" class="active">Support</a>
<?php foreach ($tenantNavItems as $item): ?>
<a href="<?= support_h((string) ($item['href'] ?? '/')) ?>" class="<?= (($item['key'] ?? '') === 'support') ? 'active' : '' ?>"><?= support_h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
<form method="post" action="/logout/"><button type="submit" class="button secondary">Abmelden</button></form>
</nav>
</header>
<section class="hero">
<p class="hero__kicker">Support Requests</p>
<h1 class="hero__title">Anfragen sichtbar machen, bearbeiten und nachvollziehbar abschließen.</h1>
<p class="hero__kicker">Support</p>
<h1 class="hero__title">Support</h1>
<p class="hero__lead">
Das Modul bildet Support als echten Vorgang ab: mit Status, Routing, Rückmeldungen und einer sauberen Sicht
für Mitglieder sowie Verantwortliche. Mitglieder sehen nur ihre eigenen Vorgänge, Verantwortliche den
gesamten Tenant-Kontext.
Mitglieder legen hier Vorgänge an. Verantwortliche verfolgen, beantworten und schliessen sie im selben Verlauf.
</p>
<div class="context">
<?= support_badge($isManager ? 'Verantwortlichen-Sicht' : 'Mitgliedersicht', 'success') ?>
<?= support_badge('Tenant-scoped') ?>
<?= support_badge('Status + Routing vorbereitet', 'warning') ?>
<?= support_badge('Tenant-weit') ?>
<?= support_badge('Status und Routing', 'warning') ?>
</div>
</section>
@@ -371,7 +367,7 @@
</section>
<?php endif; ?>
<p class="footer">Support-Modul als tenantfähiges Vorgangssystem, vorbereitet für Routing, Statuswechsel und spätere Erweiterungen.</p>
<p class="footer">Kaffeeliste | Support, Hinweise und Betriebsprozesse im Tenant-Menü</p>
</main>
</body>
</html>
@@ -1,6 +1,6 @@
@extends('layouts.app')
@section('page_title', $title ?? 'Kaffeeliste SaaS - Surveys')
@section('page_title', $title ?? 'Kaffeeliste SaaS - Umfragen')
@php
$board = $board ?? [
@@ -18,8 +18,8 @@
<section class="hero hero--split">
<div class="hero__content">
<div>
<p class="hero__kicker">Tenant Survey Admin</p>
<h2 class="hero__title">Umfragen werden tenantfaehig verwaltet und als Snapshot veroeffentlicht.</h2>
<p class="hero__kicker">Umfragen</p>
<h2 class="hero__title">Umfragen</h2>
<p class="hero__lead">
Das Survey-Modul ist jetzt nicht mehr nur eine Zusatzfunktion, sondern ein klarer
Verwaltungsbereich fuer Tenant-Admins und Survey-Manager. Entwuerfe werden live gepflegt,
@@ -1,6 +1,6 @@
@extends('layouts.app')
@section('page_title', 'Kaffeeliste SaaS - Tenant Console')
@section('page_title', 'Kaffeeliste SaaS - Verwaltung')
@php
$data = $overview ?? [
@@ -29,8 +29,8 @@
<section class="hero hero--split">
<div class="hero__content">
<div>
<p class="hero__kicker">Central admin console</p>
<h2 class="hero__title">Alle Tenants, Mitgliedschaften und Login-Pfade in einer zentralen Admin-Uebersicht.</h2>
<p class="hero__kicker">Verwaltung</p>
<h2 class="hero__title">Mandanten</h2>
<p class="hero__lead">
Diese Konsole ist die zentrale Schaltstelle fuer Tenant-Rollout, Domains, zentrale Anmeldung und den Umgang
mit Mitarbeitenden, die in mehreren Tenants hinterlegt sind. Statt verteilter Einzelansichten entsteht eine
@@ -6,19 +6,19 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kaffeeliste SaaS - Tenant Rollen</title>
<title>Kaffeeliste - Rollen</title>
<style>
:root{
--bg:#f4efe6;
--card:#fffaf3;
--ink:#22160f;
--muted:#675546;
--brand:#0c6b66;
--brand-2:#084d49;
--accent:#a34b12;
--line:rgba(34,22,15,.12);
--shadow:0 24px 54px rgba(57,35,22,.11);
--radius:24px;
--bg:#f4efe4;
--card:#fffdf8;
--ink:#25180f;
--muted:#63584a;
--brand:#005e3f;
--brand-2:#00452f;
--accent:#c18a00;
--line:rgba(37,24,15,.14);
--shadow:0 16px 36px rgba(37,24,15,.08);
--radius:18px;
}
*{box-sizing:border-box}
body{
@@ -26,34 +26,31 @@
min-height:100vh;
font-family:"Aptos","Segoe UI",sans-serif;
color:var(--ink);
background:
radial-gradient(circle at top left, rgba(163,75,18,.10), transparent 26%),
radial-gradient(circle at top right, rgba(12,107,102,.14), transparent 24%),
linear-gradient(180deg,#fbf8f2 0%,var(--bg) 100%);
background:linear-gradient(180deg,#f9f6ef 0%,var(--bg) 100%);
}
a{color:inherit;text-decoration:none}
h1,h2,h3{font-family:Georgia,serif;letter-spacing:-.02em}
.shell{width:min(1300px,calc(100vw - 32px));margin:24px auto 40px}
.shell{width:min(1240px,calc(100vw - 32px));margin:20px auto 40px}
.bar,.hero,.card,.table-card,.note{border:1px solid var(--line);border-radius:var(--radius);background:var(--card);box-shadow:var(--shadow)}
.bar{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:18px 22px;margin-bottom:18px;flex-wrap:wrap}
.brand{display:grid;gap:4px}
.brand strong{font-size:1.26rem}
.brand strong{font-size:1.18rem}
.brand span,.muted{color:var(--muted)}
.toolbar,.actions,.context,.meta{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.badge,.pill{display:inline-flex;align-items:center;padding:7px 12px;border-radius:999px;background:rgba(12,107,102,.08);color:var(--brand);font-weight:700;font-size:.86rem}
.badge--solid{background:linear-gradient(135deg,var(--brand),var(--brand-2));color:#fff}
.hero{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(260px,.6fr);gap:20px;padding:28px;margin-bottom:18px}
.badge,.pill{display:inline-flex;align-items:center;padding:7px 12px;border-radius:999px;background:#fff;color:var(--brand);font-weight:700;font-size:.86rem;border:1px solid rgba(0,94,63,.12)}
.badge--solid{background:var(--brand);color:#fff}
.hero{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(260px,.6fr);gap:20px;padding:24px;margin-bottom:18px;background:linear-gradient(180deg,#fffdf8 0%,#f9f4ea 100%)}
.hero__kicker{margin:0 0 10px;color:var(--accent);text-transform:uppercase;letter-spacing:.15em;font-size:.8rem;font-weight:800}
.hero__title{margin:0 0 12px;font-size:clamp(2rem,4vw,3.5rem);line-height:1.05}
.hero__title{margin:0 0 12px;font-size:clamp(1.9rem,4vw,3rem);line-height:1.05}
.hero__lead{margin:0;color:var(--muted);max-width:70ch;line-height:1.7}
.hero__actions{display:flex;flex-wrap:wrap;gap:12px;margin-top:16px}
.button{display:inline-flex;align-items:center;justify-content:center;padding:11px 16px;border-radius:999px;border:0;background:linear-gradient(135deg,var(--brand),var(--brand-2));color:#fff;font-weight:700}
.button--ghost{background:transparent;color:var(--brand);border:1px solid rgba(12,107,102,.18)}
.button{display:inline-flex;align-items:center;justify-content:center;padding:10px 14px;border-radius:999px;border:1px solid transparent;background:var(--brand);color:#fff;font-weight:700}
.button--ghost{background:#fff;color:var(--brand);border:1px solid rgba(0,94,63,.18)}
.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))}
.grid--4{grid-template-columns:repeat(4,minmax(0,1fr))}
.metric{padding:18px;border:1px solid var(--line);border-radius:20px;background:#fffdf8}
.metric{padding:18px;border:1px solid var(--line);border-radius:16px;background:#fff}
.metric__label{color:var(--muted)}
.metric__value{font-size:1.9rem;font-weight:800;margin:6px 0 8px}
.table-card,.card{padding:22px}
@@ -61,12 +58,12 @@
table{width:100%;border-collapse:collapse}
th,td{padding:12px 10px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}
th{font-size:.82rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}
.status{display:inline-flex;align-items:center;padding:6px 10px;border-radius:999px;background:rgba(12,107,102,.1);color:var(--brand);font-weight:700;font-size:.84rem}
.status--warning{background:rgba(163,75,18,.12);color:#98510c}
.status--success{background:rgba(17,98,61,.12);color:#11623d}
.status{display:inline-flex;align-items:center;padding:6px 10px;border-radius:999px;background:rgba(0,94,63,.1);color:var(--brand);font-weight:700;font-size:.84rem}
.status--warning{background:rgba(193,138,0,.14);color:#8c6500}
.status--success{background:rgba(0,94,63,.12);color:var(--brand)}
.list-reset{margin:0;padding-left:18px}
.list-reset li+li{margin-top:10px}
.note{padding:16px 18px;background:rgba(12,107,102,.06)}
.note{padding:16px 18px;background:rgba(0,94,63,.06)}
.stack{display:grid;gap:12px}
.chip-list{display:flex;flex-wrap:wrap;gap:8px}
.chip{display:inline-flex;align-items:center;padding:5px 10px;border-radius:999px;background:rgba(12,107,102,.08);color:var(--brand);font-size:.84rem;font-weight:700}
@@ -77,28 +74,27 @@
<main class="shell">
<header class="bar">
<div class="brand">
<strong>Kaffeeliste SaaS</strong>
<span>Tenant Rollen, Rechte und Delegation an einem Ort.</span>
<strong>Kaffeeliste</strong>
<span>Rollen und Rechte im Tenant.</span>
</div>
<div class="toolbar">
<span class="badge"><?= tenant_roles_h((string) ($tenant['name'] ?? 'Tenant')) ?></span>
<span class="badge">Tenant-Admin Vollzugriff</span>
<span class="badge badge--solid">Vier-Augen optional</span>
<?php foreach ($tenantNavItems as $item): ?>
<a class="badge <?= (($item['key'] ?? '') === 'roles') ? 'badge--solid' : '' ?>" href="<?= tenant_roles_h((string) ($item['href'] ?? '/')) ?>"><?= tenant_roles_h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
<form method="post" action="/logout/"><button type="submit" class="button button--ghost">Abmelden</button></form>
</div>
</header>
<section class="hero">
<div>
<p class="hero__kicker">Tenant Rollenmodell</p>
<h1 class="hero__title">Die Plattform zeigt klar, wer was darf und wer es weitergeben kann.</h1>
<p class="hero__kicker">Rollen</p>
<h1 class="hero__title">Rollen und Rechte</h1>
<p class="hero__lead">
Tenant-Admins behalten den Gesamtzugriff, während finance_admin, support_contact und survey_manager
als Fachrollen gezielt delegiert werden. Wenn eine Funktion noch nicht direkt in der Anwendung steckt,
wird sie als Tenant-Funktion eingerichtet und sichtbar gemacht.
Tenant-Admins behalten den Gesamtzugriff. Fachrollen für Finanzen, Support und Umfragen werden gezielt delegiert.
</p>
<div class="hero__actions">
<a class="button" href="/tenants/">Zur Tenant-Konsole</a>
<a class="button button--ghost" href="/dashboard/">Zum Tenant-Dashboard</a>
<a class="button" href="/dashboard/">Zum Dashboard</a>
<a class="button button--ghost" href="/members/">Mitglieder</a>
</div>
<div class="meta" style="margin-top:16px;">
<span class="badge">lokal + ADFS/OIDC</span>