Anpassung des Menü
This commit is contained in:
@@ -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
@@ -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">
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user