Startseite angepasst

This commit is contained in:
2026-04-04 15:09:56 +02:00
parent 0b7156af47
commit bfab08029b
8 changed files with 1306 additions and 901 deletions
@@ -4,25 +4,36 @@ declare(strict_types=1);
namespace App\Modules\Central\Controllers;
use App\Modules\Identity\Services\AuthService;
use App\Modules\Tenants\Services\TenantService;
class LandingController
{
public function __construct(
private readonly TenantService $tenantService,
private readonly AuthService $authService
) {
}
public function index(): array
{
return [
'view' => 'welcome',
'data' => [
'title' => 'Kaffeeliste SaaS',
'tenantOverview' => $this->tenantService->adminOverview(),
'centralLoginPreview' => $this->authService->centralLoginPreview(),
'title' => 'Die Kaffeeliste',
'landingColumns' => [
[
'eyebrow' => 'Für Mitglieder',
'title' => 'Schnell im Alltag',
'copy' => 'Anmelden, Überblick sehen und direkt weitermachen.',
],
[
'eyebrow' => 'Für Verantwortliche',
'title' => 'Einfach verwalten',
'copy' => 'Mitglieder, Bereiche und Abläufe an einem Ort.',
],
[
'eyebrow' => 'Für Teams',
'title' => 'Klar für alle',
'copy' => 'Eine gemeinsame Kaffeeliste mit verständlicher Oberfläche.',
],
],
'landingPreview' => [
['label' => 'Schneller Überblick', 'value' => 'Direkt sichtbar'],
['label' => 'Für Mitglieder', 'value' => 'Einfach nutzbar'],
['label' => 'Für Verantwortliche', 'value' => 'Klar verwaltet'],
],
],
];
}
+282 -115
View File
@@ -71,7 +71,6 @@ if ($page === 'logout' && $requestMethod === 'POST') {
app_redirect('/');
}
$marketing = app_marketing_messages();
$flash = app_flash();
$auth = app_auth_user();
$pdo = null;
@@ -223,7 +222,31 @@ if ($auth !== null && isset($restrictedPages[$page]) && !$restrictedPages[$page]
$canManageTenant = app_can_manage_tenant($auth);
$tenantNavItems = app_tenant_navigation_items($auth, $tenantLicense);
$guestNavItems = [
['key' => 'home', 'href' => '/', 'label' => 'Start'],
['key' => 'login', 'href' => '/login/', 'label' => 'Anmeldung'],
['key' => 'tenants', 'href' => '/admin/login/', 'label' => 'Admin'],
];
$primaryNavItems = $auth === null ? $guestNavItems : $tenantNavItems;
$currentNavLabel = 'Start';
foreach ($primaryNavItems as $item) {
if ($page === (string) ($item['key'] ?? '')) {
$currentNavLabel = (string) ($item['label'] ?? $currentNavLabel);
break;
}
}
$themeCss = app_tenant_theme_root_css($tenantSettings);
$isMarketingHome = $page === 'home' && $auth === null;
$landingColumns = [
['eyebrow' => 'Für Mitglieder', 'title' => 'Schnell starten', 'copy' => 'Einloggen und direkt weiter.'],
['eyebrow' => 'Für Verantwortliche', 'title' => 'Alles im Blick', 'copy' => 'Bereiche, Hinweise und Verwaltung an einem Ort.'],
['eyebrow' => 'Für Standorte', 'title' => 'Gemeinsam organisiert', 'copy' => 'Ein klarer Ablauf für Teams und Bereiche.'],
];
$landingPreview = [
['label' => 'Start', 'value' => 'Klar'],
['label' => 'Team', 'value' => 'Gemeinsam'],
['label' => 'Zugang', 'value' => 'Direkt'],
];
?><!DOCTYPE html>
<html lang="de">
@@ -237,104 +260,248 @@ $themeCss = app_tenant_theme_root_css($tenantSettings);
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{font-family:Georgia,serif;letter-spacing:-.02em}
.button,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:inherit;font-weight:700;cursor:pointer}
.button,button{display:inline-flex;align-items:center;justify-content:center;padding:10px 14px;border-radius:12px;border:1px solid transparent;background:var(--brand);color:#fff;font:inherit;font-weight:700;cursor:pointer}
.button.secondary,.button--ghost{background:#fff;color:var(--brand);border-color:rgba(var(--brand-rgb),.18)}
.page-shell{width:min(1460px,calc(100vw - 32px));margin:20px auto 40px;display:grid;grid-template-columns:minmax(280px,300px) minmax(0,1fr);gap:20px;align-items:start}
.sidebar,.hero,.card,.alert{border:1px solid var(--line);border-radius:var(--radius);background:var(--card);box-shadow:var(--shadow)}
.sidebar{position:sticky;top:20px;display:grid;gap:16px;padding:18px;background:rgba(255,251,244,.96)}
.sidebar__brand{display:grid;gap:6px;padding-bottom:14px;border-bottom:1px solid rgba(37,24,15,.1)}
.page-shell{width:min(1320px,calc(100vw - 32px));margin:18px auto 34px;display:grid;gap:16px}
.site-header{position:sticky;top:18px;z-index:20}
.site-header__inner{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:16px 18px;border:1px solid var(--line);border-radius:24px;background:rgba(255,255,255,.94);box-shadow:var(--shadow)}
.site-brand{display:flex;align-items:center;gap:14px;min-width:0}
.site-brand__mark{width:44px;height:44px;border-radius:16px;display:grid;place-items:center;background:linear-gradient(135deg,var(--brand) 0%,var(--brand-strong) 100%);color:#fff;font-weight:800;box-shadow:0 14px 28px rgba(var(--brand-rgb),.18)}
.site-brand__title{margin:0;font-size:1.1rem;color:var(--ink)}
.site-brand__subtitle{margin:2px 0 0;color:var(--muted);font-size:.92rem}
.site-nav,.site-actions,.actions,.context{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.site-nav__link{display:inline-flex;align-items:center;justify-content:center;padding:10px 14px;border-radius:999px;border:1px solid transparent;background:transparent;color:var(--muted);font-weight:700}
.site-nav__link:hover{text-decoration:none;color:var(--brand);background:rgba(255,255,255,.82);border-color:rgba(var(--brand-rgb),.14)}
.site-nav__link.active{background:rgba(var(--brand-rgb),.10);color:var(--brand);border-color:rgba(var(--brand-rgb),.18)}
.site-mobile{display:none;position:relative}
.site-mobile[open]{z-index:20}
.site-toggle{display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:pointer;list-style:none;padding:12px 14px;border-radius:16px;border:1px solid rgba(var(--brand-rgb),.12);background:#fff;color:var(--brand);font-weight:700}
.site-toggle::-webkit-details-marker{display:none}
.site-toggle::after{content:"";width:11px;height:11px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:rotate(45deg);transition:transform .2s ease}
.site-mobile[open] .site-toggle::after{transform:rotate(225deg)}
.site-panel{position:absolute;right:0;top:calc(100% + 12px);width:min(320px,calc(100vw - 32px));padding:14px;border-radius:18px;border:1px solid var(--line);background:#fffdf9;box-shadow:var(--shadow)}
.site-stack{display:grid;gap:10px}
.site-stack .site-nav__link{justify-content:flex-start;background:rgba(255,255,255,.78);border-color:rgba(37,24,15,.08);color:var(--ink)}
.site-footer-actions{display:flex;justify-content:flex-end;margin-top:12px}
.hero,.card,.alert{border:1px solid var(--line);border-radius:var(--radius);background:var(--card);box-shadow:var(--shadow)}
.sidebar__eyebrow,.eyebrow{display:inline-block;color:var(--accent);text-transform:uppercase;letter-spacing:.14em;font-size:.8rem;font-weight:800}
.sidebar__eyebrow{margin:0}
.sidebar__title{margin:0;font-size:1.55rem;line-height:1.05}
.sidebar__title{margin:0;font-size:1.32rem;line-height:1.04}
.sidebar__subtitle,.muted,p{color:var(--muted)}
.sidebar__meta,.actions,.context{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.sidebar__section{display:inline-block;color:var(--accent);text-transform:uppercase;letter-spacing:.14em;font-size:.76rem;font-weight:800}
.sidebar__nav,.sidebar__stack,.stack{display:grid;gap:10px}
.sidebar__link{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-radius:16px;border:1px solid rgba(var(--brand-rgb),.12);background:#fff;color:var(--brand);font-weight:700}
.sidebar__link.active{background:var(--brand);color:#fff}
.sidebar__footer{display:grid;gap:12px;padding-top:14px;border-top:1px solid rgba(37,24,15,.1)}
.sidebar__mobile{display:none}
.sidebar__mobile[open]{z-index:20}
.sidebar__toggle{display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:pointer;list-style:none;padding:12px 14px;border-radius:16px;border:1px solid rgba(var(--brand-rgb),.12);background:#fff;color:var(--brand);font-weight:700}
.sidebar__toggle::-webkit-details-marker{display:none}
.sidebar__toggle::after{content:"";width:11px;height:11px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:rotate(45deg);transition:transform .2s ease}
.sidebar__mobile[open] .sidebar__toggle::after{transform:rotate(225deg)}
.sidebar__panel{margin-top:12px;padding:14px;border-radius:18px;border:1px solid var(--line);background:#fffdf9;box-shadow:var(--shadow)}
.content{min-width:0;display:grid;gap:18px}
.hero,.card{padding:24px}
.hero{margin-bottom:0;background:linear-gradient(180deg,#fffdf8 0%,#f9f4ea 100%)}
.content{min-width:0;display:grid;gap:16px}
.hero,.card{padding:20px}
.hero{margin-bottom:0;background:rgba(255,255,255,.9)}
.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))}
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}
.metric{padding:18px;border:1px solid var(--line);border-radius:16px;background:#fff}.metric strong{display:block;font-size:1.8rem;margin-bottom:8px}
h1{font-size:clamp(1.9rem,4vw,2.8rem);margin:0 0 10px}h2{font-size:1.2rem;margin:0 0 10px}h3{font-size:1rem;margin:0 0 8px}p{margin:0;line-height:1.55}
.metric{padding:18px;border:1px solid var(--line);border-radius:16px;background:#fff}.metric strong{display:block;font-size:1.55rem;margin-bottom:6px}
.list{margin:14px 0 0;padding-left:18px;color:var(--muted);line-height:1.7}
.alert{padding:16px 18px;margin-bottom:0}.alert-success{background:rgba(var(--brand-rgb),.08)}.alert-warning{background:rgba(var(--accent-rgb),.1)}.alert-error{background:rgba(154,31,31,.1)}.alert-info{background:rgba(var(--brand-rgb),.08)}
.badge{display:inline-flex;align-items:center;padding:7px 12px;border-radius:999px;font-size:.86rem;font-weight:700}
.badge-neutral{background:rgba(var(--brand-rgb),.08);color:var(--brand)}.badge-success{background:rgba(var(--brand-rgb),.12);color:var(--brand)}.badge-warning{background:rgba(var(--accent-rgb),.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)}
.table{overflow-x:auto}.table table{width:100%;border-collapse:collapse;min-width:720px}.table th,.table td{padding:12px 10px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}.table th{font-size:.78rem;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:0;text-align:center;color:var(--muted);font-size:.92rem}
@media(max-width:960px){.page-shell{grid-template-columns:1fr;width:min(100vw - 20px,1460px)}.sidebar{position:static}.sidebar__desktop{display:none}.sidebar__mobile{display:block}.grid-2,.grid-3,.grid-4,form.grid{grid-template-columns:1fr}.table table{min-width:0}}
@media(min-width:961px){.sidebar__mobile{display:none}}
.section-mini{display:grid;gap:10px}
.section-mini__title{margin:0;font-size:1rem;color:var(--ink)}
.section-mini__copy{margin:0;color:var(--muted)}
body.landing-preview{background:radial-gradient(circle at top center,rgba(88,122,255,.22),transparent 28%),radial-gradient(circle at 20% 20%,rgba(42,72,170,.18),transparent 24%),linear-gradient(180deg,#05070d 0%,#0a0f1a 52%,#0d1320 100%);color:#f4f7ff}
body.landing-preview h1,body.landing-preview h2,body.landing-preview h3{font-family:"Inter Tight","Aptos","Segoe UI",sans-serif;letter-spacing:-.04em}
.marketing-shell{min-height:100vh;padding:0 0 92px;display:grid;gap:12px}
.marketing-main{width:min(1240px,calc(100vw - 48px));margin:0 auto}
.marketing-bar{position:sticky;top:0;z-index:20;display:flex;align-items:center;justify-content:space-between;gap:14px;height:88px;min-height:88px;width:100%;margin:0 0 20px;padding:0 18px;border:0;border-bottom:1px solid rgba(137,154,188,.18);border-radius:0;background:rgba(8,10,18,.82);box-shadow:none;backdrop-filter:blur(16px)}
.marketing-brand{display:flex;align-items:center;gap:8px;flex:0 0 auto}
.marketing-brand__mark{width:40px;height:40px;border-radius:10px;display:grid;place-items:center;background:linear-gradient(135deg,#587aff 0%,#243a91 100%);color:#fff;font-size:1rem;font-weight:800;box-shadow:none}
.marketing-brand__title{margin:0;font-size:1.08rem;line-height:1;color:#f4f7ff}
.marketing-brand__copy{display:none}
.marketing-nav,.marketing-actions,.landing-actions{display:flex;flex-wrap:wrap;gap:12px;align-items:center}
.marketing-nav{gap:6px}
.marketing-nav a{display:inline-flex;align-items:center;justify-content:center;min-height:36px;padding:0 .3rem;border-radius:6px;border:1px solid transparent;font-size:1rem;font-weight:600;color:rgba(231,238,255,.8)}
.marketing-nav a:hover{text-decoration:none;color:#f4f7ff;background:rgba(255,255,255,.06);border-color:rgba(201,214,255,.12)}
.marketing-actions .button{min-height:38px;padding:.42rem .8rem;border-radius:8px;font-size:.94rem}
.marketing-actions .button.secondary,.marketing-mobile__footer .button.secondary{background:transparent;color:#f4f7ff;border-color:rgba(201,214,255,.16);box-shadow:none}
.marketing-mobile{display:none;position:relative}
.marketing-mobile[open]{z-index:40}
.marketing-mobile__toggle{display:inline-flex;align-items:center;justify-content:center;gap:10px;list-style:none;cursor:pointer;min-width:38px;min-height:38px;padding:.24rem .46rem;border-radius:8px;border:1px solid rgba(201,214,255,.14);background:rgba(255,255,255,.04);color:#f4f7ff}
.marketing-mobile__toggle::-webkit-details-marker{display:none}
.marketing-mobile__toggle::before{content:"";width:16px;height:10px;border-top:2px solid currentColor;border-bottom:2px solid currentColor;box-shadow:inset 0 -4px 0 0 currentColor}
.marketing-mobile__panel{position:absolute;right:0;top:calc(100% + 10px);width:min(280px,calc(100vw - 24px));padding:14px;border-radius:16px;border:1px solid rgba(163,183,255,.14);background:rgba(8,10,18,.96);box-shadow:0 18px 36px rgba(1,5,13,.45)}
.marketing-mobile__stack{display:grid;gap:8px}
.marketing-mobile__stack .marketing-nav__link{justify-content:flex-start;padding:.55rem .7rem;background:rgba(255,255,255,.04);border-color:rgba(201,214,255,.1)}
.marketing-mobile__footer{margin-top:12px;display:flex;justify-content:flex-end}
.landing-hero{width:min(1240px,calc(100vw - 48px));margin:0 auto;padding-top:12px;display:grid;grid-template-columns:minmax(0,1.08fr) minmax(280px,.92fr);gap:40px;align-items:start}
.landing-copy__eyebrow{margin:0 0 10px;text-transform:uppercase;letter-spacing:.16em;font-size:.78rem;font-weight:700;color:#8aa0d0}
.landing-copy h1{margin:0;font-size:clamp(3.2rem,7vw,6.4rem);line-height:.9;letter-spacing:-.06em;color:#f4f7ff}
.landing-copy p{margin:14px 0 0;color:rgba(219,228,248,.72);font-size:1.05rem;max-width:30rem;line-height:1.65}
.landing-actions{margin-top:24px}
.landing-visual,.landing-callout{border:1px solid rgba(163,183,255,.12);border-radius:20px;background:rgba(14,20,34,.86);box-shadow:0 16px 40px rgba(2,5,12,.34)}
.landing-visual{padding:20px;display:grid;gap:12px}
.landing-visual__top{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-bottom:12px;border-bottom:1px solid rgba(163,183,255,.12)}
.landing-visual__title{margin:0;font-size:1.1rem;color:#f4f7ff}
.landing-visual__stack{display:grid;gap:12px}
.landing-visual__row{display:flex;justify-content:space-between;gap:12px;align-items:center;padding:12px 14px;border-radius:16px;background:rgba(255,255,255,.04);color:rgba(216,225,246,.72);font-weight:600}
.landing-visual__row strong{color:#f4f7ff}
.landing-columns{width:min(1240px,calc(100vw - 48px));margin:0 auto;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0;border-top:1px solid rgba(163,183,255,.12);border-bottom:1px solid rgba(163,183,255,.12)}
.landing-column{padding:24px 18px}
.landing-column + .landing-column{border-left:1px solid rgba(163,183,255,.12)}
.landing-column__eyebrow{margin:0 0 10px;color:#8aa0d0;font-size:.78rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase}
.landing-column h2{margin:0 0 10px;font-size:1.45rem;color:#f4f7ff}
.landing-column p{margin:0;max-width:22rem;color:rgba(219,228,248,.72)}
.landing-callout{width:min(1240px,calc(100vw - 48px));margin:0 auto;padding:24px 26px}
.landing-callout__eyebrow{margin:0 0 8px;color:#8aa0d0;font-size:.78rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase}
.landing-callout h2{margin:0 0 10px;font-size:2rem;color:#f4f7ff}
.landing-callout p{margin:0;max-width:36rem;color:rgba(217,226,246,.8)}
.landing-cta{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:16px;padding-top:20px;margin-top:22px;border-top:1px solid rgba(163,183,255,.12)}
.landing-cta__copy{display:grid;gap:6px}
.landing-cta__copy strong{font-size:1.05rem;color:#f4f7ff}
.landing-cta__copy span{color:rgba(217,226,246,.72)}
.marketing-footer{position:fixed;left:0;right:0;bottom:0;z-index:25;margin-top:0;padding:10px 24px;border-top:1px solid rgba(137,154,188,.14);background:rgba(8,10,18,.88);backdrop-filter:blur(18px);color:rgba(209,217,235,.68);font-size:.92rem}
.marketing-footer__inner{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;width:min(1240px,calc(100% - 48px));margin:0 auto}
@media(max-width:960px){.marketing-main,.landing-hero,.landing-columns,.landing-callout{width:calc(100vw - 24px)}.marketing-bar{height:72px;min-height:72px;margin:0 0 16px;padding:0 12px}.marketing-nav,.marketing-actions{display:none}.marketing-mobile{display:block}.landing-hero,.landing-columns{grid-template-columns:1fr}.landing-hero{gap:24px;padding-top:0}.landing-column{padding:20px 0}.landing-column + .landing-column{border-left:0;border-top:1px solid rgba(163,183,255,.12)}.landing-callout{padding:20px}.marketing-shell{padding:0 0 88px}.marketing-footer{padding:10px 12px}.marketing-footer__inner{width:100%}}
@media(max-width:960px){.page-shell{width:min(100vw - 20px,1460px)}.site-header__inner{align-items:flex-start;padding:14px 16px}.site-nav{display:none}.site-mobile{display:block}.grid-2,.grid-3,.grid-4,form.grid{grid-template-columns:1fr}.table table{min-width:0}}
@media(min-width:961px){.site-mobile{display:none}}
</style>
</head>
<body>
<main class="page-shell">
<aside class="sidebar" aria-label="Bereichsnavigation">
<div class="sidebar__brand">
<p class="sidebar__eyebrow">Die Kaffeeliste</p>
<h1 class="sidebar__title"><?= $auth === null ? 'Start' : h((string) ($auth['tenant_name'] ?? 'Tenant')) ?></h1>
<p class="sidebar__subtitle"><?= $auth === null ? 'Zentrale Anmeldung, Verwaltung und Tenant-Zugriff.' : 'Alle Bereiche des Tenants in einer gemeinsamen Navigation.' ?></p>
</div>
<?php if ($auth === null): ?>
<div class="sidebar__meta">
<?= badge('Startseite') ?>
<?= badge('Mandantenfähig', 'success') ?>
</div>
<?php endif; ?>
<div class="sidebar__desktop">
<span class="sidebar__section">Bereiche</span>
<nav class="sidebar__nav" aria-label="Navigation">
<?php if ($auth === null): ?>
<a href="/" class="sidebar__link <?= $page === 'home' ? 'active' : '' ?>" <?= $page === 'home' ? 'aria-current="page"' : '' ?>>Start</a>
<a href="/login/" class="sidebar__link <?= $page === 'login' ? 'active' : '' ?>" <?= $page === 'login' ? 'aria-current="page"' : '' ?>>Anmeldung</a>
<a href="/admin/login/" class="sidebar__link <?= $page === 'tenants' ? 'active' : '' ?>" <?= $page === 'tenants' ? 'aria-current="page"' : '' ?>>Verwaltung</a>
<?php else: ?>
<?php foreach ($tenantNavItems as $item): ?>
<a href="<?= h((string) ($item['href'] ?? '/')) ?>" class="sidebar__link <?= $page === (string) ($item['key'] ?? '') ? 'active' : '' ?>" <?= $page === (string) ($item['key'] ?? '') ? 'aria-current="page"' : '' ?>><?= h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
<?php endif; ?>
</nav>
<div class="sidebar__footer">
<?php if ($auth !== null): ?>
<form method="post" action="/logout/"><button type="submit" class="button secondary">Abmelden</button></form>
<?php endif; ?>
</div>
</div>
<details class="sidebar__mobile">
<summary class="sidebar__toggle">Menü</summary>
<div class="sidebar__panel">
<nav class="sidebar__stack" aria-label="Mobile Navigation">
<?php if ($auth === null): ?>
<a href="/" class="sidebar__link <?= $page === 'home' ? 'active' : '' ?>" <?= $page === 'home' ? 'aria-current="page"' : '' ?>>Start</a>
<a href="/login/" class="sidebar__link <?= $page === 'login' ? 'active' : '' ?>" <?= $page === 'login' ? 'aria-current="page"' : '' ?>>Anmeldung</a>
<a href="/admin/login/" class="sidebar__link <?= $page === 'tenants' ? 'active' : '' ?>" <?= $page === 'tenants' ? 'aria-current="page"' : '' ?>>Verwaltung</a>
<?php else: ?>
<?php foreach ($tenantNavItems as $item): ?>
<a href="<?= h((string) ($item['href'] ?? '/')) ?>" class="sidebar__link <?= $page === (string) ($item['key'] ?? '') ? 'active' : '' ?>" <?= $page === (string) ($item['key'] ?? '') ? 'aria-current="page"' : '' ?>><?= h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
<?php endif; ?>
</nav>
<div class="sidebar__footer">
<?php if ($auth !== null): ?>
<form method="post" action="/logout/"><button type="submit" class="button secondary">Abmelden</button></form>
<?php endif; ?>
<body class="<?= $isMarketingHome ? 'landing-preview' : '' ?>">
<?php if ($isMarketingHome): ?>
<main class="marketing-shell">
<header class="marketing-bar">
<div class="marketing-brand">
<div class="marketing-brand__mark">K</div>
<div>
<h1 class="marketing-brand__title">Die Kaffeeliste</h1>
</div>
</div>
</details>
</aside>
<nav class="marketing-nav" aria-label="Startseiten-Navigation">
<a href="/">Start</a>
<a href="/login/">Anmeldung</a>
<a href="/admin/login/">Admin</a>
</nav>
<div class="marketing-actions">
<a class="button secondary" href="/login/">Anmelden</a>
</div>
<details class="marketing-mobile">
<summary class="marketing-mobile__toggle" aria-label="Menü"></summary>
<div class="marketing-mobile__panel">
<nav class="marketing-mobile__stack" aria-label="Mobile Startseiten-Navigation">
<a href="/" class="marketing-nav__link">Start</a>
<a href="/login/" class="marketing-nav__link">Anmeldung</a>
<a href="/admin/login/" class="marketing-nav__link">Admin</a>
</nav>
<div class="marketing-mobile__footer">
<a class="button secondary" href="/login/">Anmelden</a>
</div>
</div>
</details>
</header>
<?php if ($flash !== null): ?>
<section class="alert alert-<?= h((string) ($flash['type'] ?? 'info')) ?>" style="width:min(1240px,calc(100vw - 48px));margin:0 auto;"><?= h((string) ($flash['message'] ?? '')) ?></section>
<?php endif; ?>
<section class="marketing-hero landing-hero">
<div class="landing-copy">
<p class="landing-copy__eyebrow">Die Kaffeeliste</p>
<h1>Kaffee im Team. Klar organisiert.</h1>
<p>Für Mitglieder, Verantwortliche und Standorte.</p>
<div class="landing-actions">
<a class="button" href="/login/">Anmelden</a>
<a class="button secondary" href="#bereiche">Mehr erfahren</a>
</div>
</div>
<aside class="landing-visual" aria-label="Produktvorschau">
<div class="landing-visual__top">
<h2 class="landing-visual__title">Übersicht</h2>
<span>Heute</span>
</div>
<div class="landing-visual__stack">
<?php foreach ($landingPreview as $item): ?>
<div class="landing-visual__row">
<span><?= h((string) ($item['label'] ?? '')) ?></span>
<strong><?= h((string) ($item['value'] ?? '')) ?></strong>
</div>
<?php endforeach; ?>
</div>
</aside>
</section>
<section class="landing-columns" id="bereiche">
<?php foreach ($landingColumns as $column): ?>
<article class="landing-column">
<p class="landing-column__eyebrow"><?= h((string) ($column['eyebrow'] ?? '')) ?></p>
<h2><?= h((string) ($column['title'] ?? '')) ?></h2>
<p><?= h((string) ($column['copy'] ?? '')) ?></p>
</article>
<?php endforeach; ?>
</section>
<section class="landing-callout marketing-cta">
<p class="landing-callout__eyebrow">Einfach im Alltag</p>
<h2>Ein gemeinsamer Ort für die Kaffeeliste.</h2>
<p>Weniger suchen. Weniger erklären. Klar durch den Tag.</p>
<div class="landing-cta">
<div class="landing-cta__copy">
<strong>Bereit für den Start?</strong>
<span>Direkt zur Anmeldung.</span>
</div>
<a class="button" href="/login/">Anmelden</a>
</div>
</section>
<footer class="marketing-footer">
<div class="marketing-footer__inner">
<span>Die Kaffeeliste</span>
<span>Ein ruhiger Einstieg für Teams</span>
</div>
</footer>
</main>
<?php else: ?>
<main class="page-shell">
<header class="site-header">
<div class="site-header__inner">
<a href="<?= $auth === null ? '/' : '/dashboard/' ?>" class="site-brand">
<div class="site-brand__mark">K</div>
<div>
<h1 class="site-brand__title">Die Kaffeeliste</h1>
<p class="site-brand__subtitle"><?= $auth === null ? h($currentNavLabel) : h((string) ($auth['tenant_name'] ?? 'Bereich')) ?></p>
</div>
</a>
<nav class="site-nav" aria-label="Hauptnavigation">
<?php foreach ($primaryNavItems as $item): ?>
<a href="<?= h((string) ($item['href'] ?? '/')) ?>" class="site-nav__link <?= $page === (string) ($item['key'] ?? '') ? 'active' : '' ?>" <?= $page === (string) ($item['key'] ?? '') ? 'aria-current="page"' : '' ?>><?= h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
</nav>
<div class="site-actions">
<?php if ($auth !== null): ?>
<?= badge((string) ($auth['tenant_name'] ?? 'Bereich')) ?>
<form method="post" action="/logout/"><button type="submit" class="button secondary">Abmelden</button></form>
<?php else: ?>
<a class="button secondary" href="/login/">Anmelden</a>
<?php endif; ?>
<details class="site-mobile">
<summary class="site-toggle">Menue</summary>
<div class="site-panel">
<nav class="site-stack" aria-label="Mobile Navigation">
<?php foreach ($primaryNavItems as $item): ?>
<a href="<?= h((string) ($item['href'] ?? '/')) ?>" class="site-nav__link <?= $page === (string) ($item['key'] ?? '') ? 'active' : '' ?>" <?= $page === (string) ($item['key'] ?? '') ? 'aria-current="page"' : '' ?>><?= h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
</nav>
<?php if ($auth !== null): ?>
<div class="site-footer-actions">
<form method="post" action="/logout/"><button type="submit" class="button secondary">Abmelden</button></form>
</div>
<?php endif; ?>
</div>
</details>
</div>
</div>
</header>
<div class="content">
<?php if ($auth !== null && !empty($auth['acting_as_platform_admin'])): ?>
@@ -387,13 +554,13 @@ $themeCss = app_tenant_theme_root_css($tenantSettings);
<?php endif; ?>
<?php elseif ($page === 'login'): ?>
<section class="hero">
<div class="eyebrow">Zentrale Anmeldung</div>
<div class="eyebrow">Anmeldung</div>
<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>
<p>Mit deiner E-Mail weiter.</p>
<div class="context" style="margin-top:16px">
<?= badge('E-Mail zuerst') ?>
<?= badge('Tenant-Erkennung') ?>
<?= badge('Mehrfachzuordnung') ?>
<?= badge('Einfach') ?>
<?= badge('Direkt') ?>
<?= badge('Klar') ?>
</div>
</section>
@@ -408,7 +575,7 @@ $themeCss = app_tenant_theme_root_css($tenantSettings);
<article class="card">
<?php if ($loginStep === 'choose-tenant'): ?>
<div class="eyebrow">Schritt 2</div>
<h2>Passenden Tenant wählen</h2>
<h2>Bereich waehlen</h2>
<div class="stack">
<?php foreach (($loginState['memberships'] ?? []) as $membership): ?>
<form method="post" action="/login/" class="card">
@@ -424,7 +591,7 @@ $themeCss = app_tenant_theme_root_css($tenantSettings);
<?php elseif ($loginStep === 'password' && is_array($selectedMembership)): ?>
<div class="eyebrow">Schritt 2</div>
<h2>Passwort eingeben</h2>
<p>Der Tenant wurde erkannt: <?= h((string) ($selectedMembership['tenant_name'] ?? '')) ?></p>
<p><?= h((string) ($selectedMembership['tenant_name'] ?? '')) ?></p>
<form method="post" action="/login/" class="stack" style="margin-top:16px">
<input type="hidden" name="action" value="authenticate">
<label>Passwort<input type="password" name="password" autocomplete="current-password" placeholder="Passwort eingeben"></label>
@@ -444,11 +611,11 @@ $themeCss = app_tenant_theme_root_css($tenantSettings);
<?php endif; ?>
</article>
<article class="card">
<div class="eyebrow">Hilfe beim Einstieg</div>
<h2>Wenn die Anmeldung nicht sofort klappt</h2>
<div class="eyebrow">Hilfe</div>
<h2>Wenn es nicht direkt klappt</h2>
<ul class="list">
<li>Prüfe zuerst, ob du die richtige berufliche E-Mail-Adresse verwendest.</li>
<li>Wenn du in mehreren Bereichen arbeitest, wirst du automatisch zur passenden Auswahl geführt.</li>
<li>Wenn du in mehreren Bereichen arbeitest, waehle den passenden aus.</li>
<li>Falls kein Zugang gefunden wird, brauchst du meist nur eine Einladung oder den Kontakt zur verantwortlichen Person.</li>
</ul>
</article>
@@ -495,13 +662,12 @@ $themeCss = app_tenant_theme_root_css($tenantSettings);
<?php endif; ?>
<?php elseif ($page === 'dashboard'): ?>
<section class="hero">
<div class="eyebrow">Tenant-Dashboard</div>
<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>
<div class="eyebrow">Uebersicht</div>
<h1><?= h((string) $auth['tenant_name']) ?></h1>
<p>Alles Wichtige auf einen Blick.</p>
<?php if (app_is_platform_admin($auth) && app_admin_user() !== null): ?>
<div class="actions" style="margin-top:18px">
<a class="button secondary" href="/admin/">Zur zentralen Verwaltung</a>
<span class="badge">Mitglieder im Menü</span>
<a class="button secondary" href="/admin/">Zur Verwaltung</a>
</div>
<?php endif; ?>
</section>
@@ -519,18 +685,18 @@ $themeCss = app_tenant_theme_root_css($tenantSettings);
<section class="grid grid-2" style="margin-top:18px">
<article class="card">
<div class="eyebrow">Bereiche im Menü</div>
<h2>Funktionen werden über das Menü geöffnet</h2>
<ul class="list">
<li>Hinweise, Support und Umfragen erscheinen als feste Hauptbereiche im Tenant-Menü.</li>
<li>Mitglieder, Rollen, Buchungen und Zahlungen werden nur für passende Rollen eingeblendet.</li>
<li>Einstellungen und Exporte erscheinen nur, wenn das Lizenzmodell diese Funktionen freischaltet.</li>
</ul>
<div class="eyebrow">Heute</div>
<h2>Schnell weiter</h2>
<div class="section-mini">
<p class="section-mini__copy">Mitglieder, Buchungen und weitere Bereiche oeffnest du direkt ueber das Menue.</p>
</div>
</article>
<article class="card">
<div class="eyebrow">Lizenzlogik</div>
<h2>Nur passende Funktionen sichtbar</h2>
<p>Die Oberfläche blendet Bereiche nicht als tote Kacheln ein. Was im Menü erscheint, hängt von Rolle und Lizenz des Tenants ab.</p>
<div class="eyebrow">Status</div>
<h2>Alles im Blick</h2>
<div class="section-mini">
<p class="section-mini__copy">Die wichtigsten Zahlen und die letzten Buchungen stehen hier zuerst.</p>
</div>
</article>
</section>
@@ -563,18 +729,18 @@ $themeCss = app_tenant_theme_root_css($tenantSettings);
</article>
<?php else: ?>
<article class="card">
<div class="eyebrow">Mitgliedersicht</div>
<div class="eyebrow">Dein Bereich</div>
<h2>Dein Bereich</h2>
<ul class="list">
<li>Hier siehst du deinen Kontostand, letzte Buchungen und aktuelle Hinweise.</li>
<li>Tenant-weite Buchungen und Mitgliederverwaltung bleiben bei den verantwortlichen Admins.</li>
<li>Weitere Bereiche findest du direkt im Menue.</li>
<li>Wenn dir etwas fehlt, wende dich an den Betreiber deines Bereichs.</li>
</ul>
</article>
<article class="card">
<div class="eyebrow">Nächster Schritt</div>
<h2>Womit du direkt weiterkommst</h2>
<p>Weitere Bereiche wechselst du direkt über das Menü. Sichtbar wird dort nur, was für deine Rolle freigegeben ist.</p>
<div class="eyebrow">Naechster Schritt</div>
<h2>Direkt weiter</h2>
<p>Nutze das Menue fuer den naechsten Bereich.</p>
</article>
<?php endif; ?>
</section>
@@ -850,8 +1016,9 @@ $themeCss = app_tenant_theme_root_css($tenantSettings);
<section class="alert alert-warning">Die angeforderte Seite konnte nicht gefunden werden.</section>
<?php endif; ?>
<p class="footer">Die Kaffeeliste | zentrale Anmeldung, Tenant-Verwaltung und alle Kernprozesse der modernen Kaffeeliste</p>
<p class="footer">Die Kaffeeliste</p>
</div>
</main>
<?php endif; ?>
</body>
</html>
+39 -221
View File
@@ -1,81 +1,22 @@
@extends('layouts.app')
@section('page_title', 'Die Kaffeeliste - Zentrale Anmeldung')
@section('page_title', 'Die Kaffeeliste - Anmeldung')
@php
$preview = $loginPreview ?? [
'single' => [
'email' => 'mia@berlin.example',
'message' => 'Direkte Weiterleitung in den zugeordneten Tenant.',
'message' => 'Direkt weiter.',
'matches' => [['name' => 'Werk Berlin', 'domain' => 'berlin.kaffeeliste.de', 'login_mode' => 'oidc-first']],
],
'multiple' => [
'email' => 'leitung@kaffeeliste.example',
'message' => 'Tenant-Auswahl vor Passwort oder SSO.',
'message' => 'Auswahl vor dem Einstieg.',
'matches' => [
['name' => 'Werk Berlin', 'domain' => 'berlin.kaffeeliste.de', 'login_mode' => 'oidc-first'],
['name' => 'Werk Koeln', 'domain' => 'koeln.kaffeeliste.de', 'login_mode' => 'oidc-first'],
['name' => 'Shared Services', 'domain' => 'shared.kaffeeliste.de', 'login_mode' => 'central-routing'],
],
],
'unknown' => [
'email' => 'extern@example.org',
'message' => 'Rueckfuehrung auf Tenant-Admin oder Einladungspfad.',
'matches' => [],
],
];
$identityPolicy = $identityPolicy ?? [
'local_login_required' => true,
'external_login_mode' => 'ADFS/OIDC',
'supported_modes' => ['password', 'adfs_oidc'],
'email_mapping' => 'E-Mail-Mapping als primaerer Abgleich zwischen externer Identitaet und internem Benutzer',
'fallback' => 'Bei Dubletten oder unklaren Zuordnungen wird ein Admin-Klaerfall erzeugt.',
'recovery' => 'Lokale Anmeldung bleibt immer verfuegbar, auch wenn externe Provider ausfallen.',
'clarification' => 'Der Erstlogin soll bestehende Konten wiederverwenden und nicht stillschweigend neue Duplikate anlegen.',
'admin_clarification' => [
'title' => 'Admin-Klaerfall',
'description' => 'Wenn eine E-Mail-Adresse nicht eindeutig zugeordnet werden kann, wird der Fall an die zuständigen Admins eskaliert.',
'steps' => ['Identitaet pruefen', 'Tenant zuordnen', 'Konto bestaetigen oder neu anlegen'],
],
];
$roleMatrix = $roleMatrix ?? [
'roles' => [
['key' => 'platform_admin', 'summary' => 'Globaler Zugriff auf Plattform, Tenants und Betriebssteuerung.'],
['key' => 'tenant_admin', 'summary' => 'Vollzugriff im eigenen Tenant inklusive Rollen- und Inhaltsverwaltung.'],
['key' => 'finance_admin', 'summary' => 'Operative Buchungsrolle fuer Einzahlungen, Storno und Finanzreports.'],
['key' => 'support_contact', 'summary' => 'Bearbeitung und Abschluss von Supportvorgaengen.'],
['key' => 'survey_manager', 'summary' => 'Pflege, Freigabe und Auswertung von Surveys.'],
['key' => 'member', 'summary' => 'Mitglied mit Zugriff auf eigenes Profil und freigegebene Inhalte.'],
],
'rules' => [
'Tenant-Admin hat Vollzugriff im eigenen Tenant.',
'Spezialrollen erhalten nur die fuer ihr Modul noetigen Rechte.',
'Einzahlungen duerfen nur von Verantwortlichen storniert werden.',
'Stricheintraege duerfen nur von Verantwortlichen geloescht werden.',
],
];
$providers = $providers ?? [
'password' => [
'label' => 'Lokale Systemanmeldung',
'type' => 'password',
'required' => true,
'description' => 'Pflichtweg fuer lokale Konten und Fallback bei externen Ausfaellen.',
],
'adfs_oidc' => [
'label' => 'ADFS / OIDC',
'type' => 'oidc',
'required' => false,
'description' => 'Zusatzoption fuer angebundene Identitaetsprovider.',
],
];
$oidcProviders = $oidcProviders ?? [
[
'provider_key' => 'adfs-oidc-default',
'driver' => 'oidc',
'client_id' => 'tenant-client-id',
'redirect_uri' => '/auth/oidc/adfs-oidc-default/callback',
'scopes' => ['openid', 'profile', 'email'],
],
];
@endphp
@@ -83,53 +24,37 @@
<section class="hero hero--split">
<div class="hero__content">
<div>
<p class="hero__kicker">Mitglieder-Login</p>
<h2 class="hero__title">Eine zentrale Anmeldung, die Mitglieder automatisch in den richtigen Tenant bringt.</h2>
<p class="hero__kicker">Anmeldung</p>
<h2 class="hero__title">Einfach einsteigen.</h2>
<p class="hero__lead">
Statt verschiedene Tenant-URLs zu kennen, starten Mitglieder zentral mit ihrer E-Mail-Adresse. Die Plattform
erkennt Einzel- oder Mehrfachzuordnungen und fuehrt danach passend weiter: direkt in den Tenant, in eine
Tenant-Auswahl oder in einen klaren Kontaktpfad.
E-Mail eingeben, kurz pruefen, weitermachen.
</p>
</div>
<div class="hero__actions">
<a class="button" href="#mitglied-login">Mitglieder-Login starten</a>
<a class="button button--ghost" href="/tenants">Admin Console ansehen</a>
</div>
<div class="hero__meta">
<span class="badge">E-Mail first</span>
<span class="badge">Tenant-Erkennung</span>
<span class="badge">Lokaler Login Pflicht</span>
<span class="badge">ADFS/OIDC zusaetzlich</span>
<span class="badge badge--solid">Mehrfachzuordnung unterstuetzt</span>
<span class="badge">Direkt</span>
<span class="badge">Klar</span>
<span class="badge badge--solid">Wenig Schritte</span>
</div>
</div>
<aside class="hero__aside">
<article class="card metric metric--compact">
<p class="metric__label">Ein Tenant erkannt</p>
<div class="metric__value">1 Klick</div>
<p class="muted">Bei genau einer Mitgliedschaft wird direkt in den passenden Tenant weitergeleitet.</p>
<p class="metric__label">Ein Treffer</p>
<div class="metric__value">Direkt</div>
<p class="muted">Ohne Umweg zum Ziel.</p>
</article>
<article class="card metric metric--compact">
<p class="metric__label">Mehrfachzuordnung</p>
<div class="metric__value">{{ count($preview['multiple']['matches']) }} Tenants</div>
<p class="muted">Die Auswahl erscheint nur dann, wenn fuer dieselbe Mail-Adresse mehrere Kontexte hinterlegt sind.</p>
<p class="metric__label">Mehrere Treffer</p>
<div class="metric__value">{{ count($preview['multiple']['matches']) }}</div>
<p class="muted">Dann Auswahl anzeigen.</p>
</article>
<div class="callout">
<strong>SSO und Passwort aus einem Einstieg</strong>
Die zentrale Anmeldung entscheidet zuerst ueber den Tenant-Kontext. Danach folgen Passwort-Login, OIDC oder ein
klarer Fallback fuer kleinere Tenants.
</div>
</aside>
</section>
<section id="mitglied-login" class="split">
<section class="split" style="margin-top: 18px;">
<article class="form-panel">
<p class="card__eyebrow">Zentraler Einstieg</p>
<h3>Mit E-Mail und Passwort anmelden</h3>
<p class="muted">
Die E-Mail-Adresse steuert zuerst die Tenant-Erkennung. Der Passwort- oder SSO-Schritt kommt danach im richtigen Kontext.
</p>
<p class="card__eyebrow">Zugang</p>
<h3>Anmelden</h3>
<form class="form-grid" method="post" action="/login" style="margin-top: 18px;">
<div class="field">
<label for="email">E-Mail-Adresse</label>
@@ -140,86 +65,60 @@
<input id="password" name="password" type="password" autocomplete="current-password" placeholder="••••••••">
</div>
<div class="toolbar">
<button type="submit">Zentral anmelden</button>
<button type="submit">Weiter</button>
<a class="button button--ghost" href="/forgot-password">Passwort vergessen?</a>
</div>
</form>
<div class="note" style="margin-top: 18px;">
Zielbild: Eine zentrale Anmeldung fuer Mitglieder, ohne dass zuerst Tenant-Keys oder Subdomains bekannt sein muessen.
</div>
</article>
<article class="panel">
<p class="card__eyebrow">Was danach passiert</p>
<h3>Automatische Weiterleitung oder Tenant-Auswahl.</h3>
<p class="card__eyebrow">Ablauf</p>
<h3>So geht es weiter</h3>
<div class="timeline timeline--tight" style="margin-top: 18px;">
<div class="timeline__item">
<p class="timeline__title">Genau ein Treffer</p>
<p class="timeline__meta">Die Plattform leitet direkt in den Tenant weiter und zeigt dort Passwort- oder SSO-Login an.</p>
<p class="timeline__title">1. E-Mail</p>
<p class="timeline__meta">Die Anmeldung startet mit der Adresse.</p>
</div>
<div class="timeline__item">
<p class="timeline__title">Mehrere Treffer</p>
<p class="timeline__meta">Vor dem eigentlichen Login erscheint eine Tenant-Auswahl mit Namen, Domain und Login-Modell.</p>
<p class="timeline__title">2. Pruefen</p>
<p class="timeline__meta">Der passende Zugang wird zugeordnet.</p>
</div>
<div class="timeline__item">
<p class="timeline__title">Kein Treffer</p>
<p class="timeline__meta">Statt Sackgasse gibt es einen klaren Rueckweg zu Tenant-Admin, Einladung oder Support.</p>
<p class="timeline__title">3. Weiter</p>
<p class="timeline__meta">Dann folgt der eigentliche Login.</p>
</div>
</div>
<div class="stack" style="margin-top:18px;">
@foreach ($providers as $providerKey => $provider)
<div class="feature-list__item" style="align-items:flex-start;">
<div class="feature-list__badge">{{ strtoupper(substr((string) $providerKey, 0, 1)) }}</div>
<div>
<p class="feature-list__title">{{ $provider['label'] ?? $providerKey }}</p>
<p class="feature-list__copy muted">{{ $provider['description'] ?? '' }}</p>
</div>
<span class="status {{ !empty($provider['required']) ? 'status--warning' : 'status--neutral' }}">
{{ !empty($provider['required']) ? 'Pflicht' : 'Optional' }}
</span>
</div>
@endforeach
</div>
<div class="callout" style="margin-top:18px;">
<strong>{{ $identityPolicy['admin_clarification']['title'] ?? 'Admin-Klaerfall' }}</strong>
<p class="muted" style="margin-top:8px;">{{ $identityPolicy['admin_clarification']['description'] ?? $identityPolicy['fallback'] ?? '' }}</p>
<ul class="list-reset" style="margin-top:12px;">
@foreach (($identityPolicy['admin_clarification']['steps'] ?? []) as $step)
<li style="margin-bottom:8px;">- {{ $step }}</li>
@endforeach
</ul>
</div>
</article>
</section>
<section class="grid grid--3" style="margin-top: 18px;">
<article class="card">
<p class="card__eyebrow">Preview: Einzelzuordnung</p>
<p class="card__eyebrow">Ein Treffer</p>
<h3>{{ $preview['single']['email'] }}</h3>
<p class="muted">{{ $preview['single']['message'] }}</p>
<div class="tenant-grid" style="margin-top: 18px;">
<div class="tenant-grid" style="margin-top: 16px;">
@foreach ($preview['single']['matches'] as $tenant)
<div class="tenant-row">
<div class="tenant-row__meta">
<p class="tenant-row__title">{{ $tenant['name'] }}</p>
<p class="tenant-row__copy">{{ $tenant['domain'] }}</p>
</div>
<span class="status">Auto-Weiterleitung</span>
<span class="status">Direkt</span>
</div>
@endforeach
</div>
</article>
<article class="card">
<p class="card__eyebrow">Preview: Mehrfachzuordnung</p>
<p class="card__eyebrow">Mehrere Treffer</p>
<h3>{{ $preview['multiple']['email'] }}</h3>
<p class="muted">{{ $preview['multiple']['message'] }}</p>
<div class="tenant-grid" style="margin-top: 18px;">
<div class="tenant-grid" style="margin-top: 16px;">
@foreach ($preview['multiple']['matches'] as $tenant)
<div class="tenant-row">
<div class="tenant-row__meta">
<p class="tenant-row__title">{{ $tenant['name'] }}</p>
<p class="tenant-row__copy">{{ $tenant['domain'] }} - {{ $tenant['login_mode'] }}</p>
<p class="tenant-row__copy">{{ $tenant['domain'] }}</p>
</div>
<span class="status status--warning">Auswahl</span>
</div>
@@ -228,94 +127,13 @@
</article>
<article class="card">
<p class="card__eyebrow">Preview: Unbekannte Mail</p>
<h3>{{ $preview['unknown']['email'] }}</h3>
<p class="muted">{{ $preview['unknown']['message'] }}</p>
<div class="callout" style="margin-top: 18px;">
<strong>Keine Sackgasse fuer Mitglieder</strong>
Die Anwendung kann stattdessen Tenant-Kontakt, Einladung oder den zentralen Supportpfad anzeigen.
<p class="card__eyebrow">Kein Treffer</p>
<h3>Sauberer Rueckweg</h3>
<p class="muted">Wenn keine Zuordnung passt, geht es klar weiter statt im Leeren zu enden.</p>
<div class="callout" style="margin-top: 16px;">
<strong>Kontakt oder Einladung</strong>
Die Anmeldung zeigt dann den naechsten klaren Schritt.
</div>
</article>
</section>
<section class="grid grid--2" style="margin-top: 18px;">
<article class="panel">
<p class="card__eyebrow">Identity-Policy</p>
<h3>Lokale Anmeldung plus ADFS/OIDC</h3>
<div class="timeline timeline--tight" style="margin-top: 18px;">
<div class="timeline__item">
<p class="timeline__title">Pflicht</p>
<p class="timeline__meta">{{ $identityPolicy['local_login_required'] ? 'Lokale Anmeldung bleibt immer verfuegbar.' : 'Lokale Anmeldung ist optional.' }}</p>
</div>
<div class="timeline__item">
<p class="timeline__title">Zusaetzliche Option</p>
<p class="timeline__meta">{{ $identityPolicy['external_login_mode'] ?? 'ADFS/OIDC' }}</p>
</div>
<div class="timeline__item">
<p class="timeline__title">Abgleich</p>
<p class="timeline__meta">{{ $identityPolicy['email_mapping'] ?? '' }}</p>
</div>
<div class="timeline__item">
<p class="timeline__title">Klärfall</p>
<p class="timeline__meta">{{ $identityPolicy['fallback'] ?? '' }}</p>
</div>
<div class="timeline__item">
<p class="timeline__title">Admin-Klaerfall</p>
<p class="timeline__meta">{{ $identityPolicy['admin_clarification']['description'] ?? '' }}</p>
</div>
</div>
</article>
<article class="panel">
<p class="card__eyebrow">Rollenmodell</p>
<h3>Vorgeschlagene Rechtebasis</h3>
<div class="stack" style="margin-top: 18px;">
@foreach ($roleMatrix['roles'] ?? [] as $role)
<div class="feature-list__item">
<div class="feature-list__badge">{{ strtoupper(substr((string) ($role['key'] ?? ''), 0, 1)) }}</div>
<div>
<p class="feature-list__title">{{ $role['key'] ?? '' }}</p>
<p class="feature-list__copy muted">{{ $role['summary'] ?? '' }}</p>
</div>
</div>
@endforeach
</div>
</article>
</section>
<section class="grid grid--2" style="margin-top: 18px;">
<article class="panel">
<p class="card__eyebrow">Provider-Details</p>
<h3>Konfigurierte ADFS/OIDC-Verbindungen</h3>
<div class="stack" style="margin-top: 18px;">
@foreach ($oidcProviders as $provider)
<div class="timeline__item">
<p class="timeline__title">{{ $provider['provider_key'] ?? 'provider' }}</p>
<p class="timeline__meta">Driver: {{ $provider['driver'] ?? 'oidc' }} | Client-ID: {{ $provider['client_id'] ?? '-' }}</p>
<p class="timeline__meta">Redirect: {{ $provider['redirect_uri'] ?? '-' }}</p>
<p class="timeline__meta">Scopes: {{ implode(', ', $provider['scopes'] ?? []) }}</p>
</div>
@endforeach
</div>
</article>
<article class="panel">
<p class="card__eyebrow">Mindestregel</p>
<h3>Lokale Anmeldung bleibt immer verfuegbar.</h3>
<div class="callout" style="margin-top: 18px;">
Selbst wenn ein externer Provider ausfaellt, bleibt der lokale Weg aktiv. So kann der Tenant weiterarbeiten und der Admin-Klaerfall bleibt handhabbar.
</div>
</article>
</section>
<section class="panel" style="margin-top: 18px;">
<h3>Regeln fuer Rollen und Abgrenzung</h3>
<ul class="list-reset">
@foreach ($roleMatrix['rules'] ?? [] as $rule)
<li style="margin-bottom: 12px;">
<span class="status">Regel</span> {{ $rule }}
</li>
@endforeach
</ul>
</section>
@endsection
@@ -3,42 +3,56 @@
@section('page_title', 'Die Kaffeeliste - Übersicht')
@section('content')
<section class="hero">
<div>
<p class="hero__kicker">Übersicht</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 können später direkt aus dem Ledger gespeist werden.
</p>
</div>
<div class="toolbar">
<span class="badge">Letzte Synchronisation: heute</span>
<span class="badge">Tenant: Demo-Workspace</span>
<span class="badge badge--solid">Saldo aktiv</span>
<section class="hero hero--split">
<div class="hero__content">
<div>
<p class="hero__kicker">Uebersicht</p>
<h2 class="hero__title">Dein Bereich</h2>
<p class="hero__lead">
Die wichtigsten Werte auf einen Blick.
</p>
</div>
<div class="hero__meta">
<span class="badge">Heute</span>
<span class="badge">Aktiv</span>
<span class="badge badge--solid">Bereit</span>
</div>
</div>
<aside class="hero__aside">
<article class="card metric metric--compact">
<p class="metric__label">Status</p>
<div class="metric__value">OK</div>
<p class="muted">Alles laeuft normal.</p>
</article>
<article class="card metric metric--compact">
<p class="metric__label">Letzte Aktivitaet</p>
<div class="metric__value">Heute</div>
<p class="muted">Neueste Aenderungen im Bereich.</p>
</article>
</aside>
</section>
<section class="grid grid--4">
<article class="card metric">
<p class="metric__label">Kontostand</p>
<div class="metric__value">7,50 EUR</div>
<p class="muted">Positiver Puffer für den laufenden Monat.</p>
<p class="muted">Aktueller Stand.</p>
</article>
<article class="card metric">
<p class="metric__label">Striche diesen Monat</p>
<p class="metric__label">Striche</p>
<div class="metric__value">5</div>
<p class="muted">Verbrauch seit Monatsbeginn.</p>
<p class="muted">Diesen Monat.</p>
</article>
<article class="card metric">
<p class="metric__label">Einzahlungen diesen Monat</p>
<p class="metric__label">Einzahlungen</p>
<div class="metric__value">1</div>
<p class="muted">Alle gebuchten Zahlungen im aktuellen Zeitraum.</p>
<p class="muted">Diesen Monat.</p>
</article>
<article class="card metric">
<p class="metric__label">Letzte Buchung</p>
<div class="metric__value">Heute</div>
<p class="muted">Aktuelle Aktivität im Mandantenbereich.</p>
<p class="muted">Aktuelle Aktivitaet.</p>
</article>
</section>
@@ -46,10 +60,10 @@
<article class="table-card">
<div class="table-card__header">
<div>
<p class="card__eyebrow">Aktivität</p>
<p class="card__eyebrow">Aktivitaet</p>
<h3>Letzte Buchungen</h3>
</div>
<span class="pill">Live feed</span>
<span class="pill">Live</span>
</div>
<div class="table-card__body">
<table>
@@ -86,24 +100,24 @@
</article>
<article class="panel">
<h3>Arbeitsbereich</h3>
<p class="muted">Die Navigation läuft vollständig über das Menü. Im Inhalt bleiben nur die fachlichen Schwerpunkte.</p>
<h3>Bereiche</h3>
<p class="muted">Im Menue liegen die zentralen Arbeitsbereiche.</p>
<div class="timeline timeline--tight">
<div class="timeline__item">
<p class="timeline__title">Mitglieder und Rollen</p>
<p class="timeline__meta">Im Menü sichtbar, wenn die angemeldete Rolle den Bereich verwalten darf.</p>
<p class="timeline__title">Mitglieder</p>
<p class="timeline__meta">Verwaltung im Hintergrund.</p>
</div>
<div class="timeline__item">
<p class="timeline__title">Buchungen und Zahlungen</p>
<p class="timeline__meta">Nur für Finanzrollen freigeschaltet und dadurch nicht für jede Anmeldung sichtbar.</p>
<p class="timeline__title">Buchungen</p>
<p class="timeline__meta">Striche und Zahlungen zusammen.</p>
</div>
<div class="timeline__item">
<p class="timeline__title">Exporte und Zusatzfunktionen</p>
<p class="timeline__meta">Werden im Menü nur angezeigt, wenn das Lizenzmodell sie für den Tenant enthält.</p>
<p class="timeline__title">Exporte</p>
<p class="timeline__meta">Nur wenn freigeschaltet.</p>
</div>
</div>
<div class="note" style="margin-top: 18px;">
Der alte Funktionskern bleibt sichtbar: Saldo, Striche, Einzahlungen und letzte Aktionen bilden die produktive Achse.
Die wichtigsten Zahlen sind hier, der Rest bleibt im Menue.
</div>
</article>
</section>
+447 -154
View File
@@ -20,10 +20,28 @@
$layoutNavItems = function_exists('app_tenant_navigation_items')
? app_tenant_navigation_items($layoutAuth, $layoutLicense)
: [];
$layoutGuestItems = [
['href' => '/', 'label' => 'Start'],
['href' => '/login/', 'label' => 'Anmeldung'],
['href' => '/admin/login/', 'label' => 'Admin'],
];
$layoutPrimaryNavItems = $layoutNavItems !== [] ? $layoutNavItems : $layoutGuestItems;
$layoutCurrentLabel = 'Start';
foreach ($layoutPrimaryNavItems as $item) {
$itemHref = rtrim((string) ($item['href'] ?? '/'), '/');
$itemHref = $itemHref === '' ? '/' : $itemHref;
if ($layoutPath === $itemHref) {
$layoutCurrentLabel = (string) ($item['label'] ?? $layoutCurrentLabel);
break;
}
}
$layoutThemeCss = function_exists('app_tenant_theme_root_css')
? app_tenant_theme_root_css($layoutThemeSettings)
: '';
@endphp
@php
$resolvedLayoutMode = trim((string) $__env->yieldContent('layout_mode', 'app'));
@endphp
<style>
:root {
{!! $layoutThemeCss !!}
@@ -64,28 +82,7 @@
margin: 14px auto;
min-height: calc(100vh - 28px);
display: grid;
grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
gap: 18px;
}
.app-sidebar {
position: sticky;
top: 14px;
align-self: start;
min-height: calc(100vh - 28px);
}
.app-sidebar__panel {
height: calc(100vh - 28px);
display: grid;
grid-template-rows: auto auto 1fr auto;
gap: 20px;
padding: 24px 20px;
border: 1px solid var(--line);
border-radius: 30px;
background:
linear-gradient(180deg, rgba(255, 252, 246, 0.98), rgba(249, 242, 231, 0.96));
box-shadow: var(--shadow);
gap: 16px;
}
.brand {
@@ -96,9 +93,9 @@
}
.brand__mark {
width: 46px;
height: 46px;
border-radius: 16px;
width: 42px;
height: 42px;
border-radius: 14px;
display: grid;
place-items: center;
font-weight: 700;
@@ -108,8 +105,82 @@
}
.brand__text { display: grid; gap: 3px; min-width: 0; }
.brand__title { margin: 0; font-size: 1.16rem; font-weight: 700; letter-spacing: 0.01em; }
.brand__title { margin: 0; font-size: 1.06rem; font-weight: 700; letter-spacing: 0.01em; }
.brand__subtitle { margin: 0; color: var(--muted); font-size: 0.92rem; }
.brand--link { color: inherit; text-decoration: none; }
.brand--link:hover { text-decoration: none; }
.site-shell {
min-height: 100vh;
padding: 14px 0 24px;
}
.site-header,
.site-main,
.app-footer {
width: min(var(--content-width), calc(100% - 28px));
margin: 0 auto;
}
.site-header {
position: sticky;
top: 14px;
z-index: 30;
margin-bottom: 18px;
}
.site-header__inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
padding: 16px 18px;
border: 1px solid var(--line);
border-radius: 22px;
background: rgba(255, 251, 244, 0.92);
box-shadow: var(--shadow);
}
.top-nav {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
min-width: 0;
}
.top-nav__link {
display: inline-flex;
align-items: center;
padding: 0.62rem 0.92rem;
border-radius: 999px;
border: 1px solid transparent;
color: var(--muted);
font-size: 0.92rem;
font-weight: 700;
white-space: nowrap;
}
.top-nav__link:hover {
text-decoration: none;
color: var(--brand-strong);
background: rgba(255, 255, 255, 0.78);
border-color: rgba(var(--brand-rgb), 0.12);
}
.top-nav__link.is-active {
color: var(--brand-strong);
background: rgba(var(--brand-rgb), 0.08);
border-color: rgba(var(--brand-rgb), 0.14);
}
.site-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
flex-wrap: wrap;
}
.sidebar-meta,
.header-meta {
@@ -135,25 +206,21 @@
.badge--solid { background: var(--brand); color: #fff; border-color: transparent; }
.sidebar-nav {
display: grid;
gap: 8px;
align-content: start;
}
.sidebar-nav { display: grid; gap: 6px; align-content: start; }
.sidebar-nav__link {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 0.88rem 1rem;
border-radius: 18px;
padding: 0.76rem 0.92rem;
border-radius: 14px;
border: 1px solid rgba(44, 32, 23, 0.08);
background: rgba(255, 255, 255, 0.72);
color: var(--text);
font-size: 0.95rem;
font-size: 0.92rem;
font-weight: 700;
box-shadow: var(--shadow-soft);
box-shadow: none;
}
.sidebar-nav__link::after {
@@ -165,7 +232,7 @@
.sidebar-nav__link:hover {
text-decoration: none;
border-color: rgba(var(--brand-rgb), 0.22);
background: rgba(255, 255, 255, 0.95);
background: rgba(255, 255, 255, 0.92);
}
.sidebar-nav__link.is-active {
@@ -178,16 +245,17 @@
.sidebar-footer {
display: grid;
gap: 12px;
gap: 10px;
align-content: end;
}
.sidebar-note {
padding: 14px 16px;
border-radius: 18px;
background: rgba(var(--brand-rgb), 0.08);
border: 1px solid rgba(var(--brand-rgb), 0.12);
padding: 12px 14px;
border-radius: 14px;
background: rgba(var(--brand-rgb), 0.06);
border: 1px solid rgba(var(--brand-rgb), 0.1);
color: var(--brand-strong);
font-size: 0.92rem;
}
.sidebar-note strong {
@@ -198,38 +266,9 @@
.app-body {
min-width: 0;
display: grid;
grid-template-rows: auto 1fr auto;
gap: 18px;
}
.context-bar { padding-top: 6px; }
.context-bar__inner {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding: 20px 24px;
border: 1px solid var(--line);
border-radius: 28px;
background: rgba(255, 251, 244, 0.92);
box-shadow: var(--shadow);
}
.context-copy { display: grid; gap: 6px; }
.context-copy__title {
margin: 0;
font-size: 1.5rem;
line-height: 1.1;
}
.context-copy__lead {
margin: 0;
color: var(--muted);
font-size: 0.95rem;
}
.mobile-nav {
display: none;
position: relative;
@@ -280,6 +319,14 @@
gap: 10px;
}
.mobile-nav__stack .top-nav__link {
justify-content: flex-start;
padding: 0.78rem 0.92rem;
border: 1px solid rgba(44, 32, 23, 0.08);
background: rgba(255, 255, 255, 0.78);
color: var(--text);
}
.mobile-nav__footer {
margin-top: 12px;
display: flex;
@@ -291,8 +338,8 @@
input[type="submit"] {
appearance: none;
border: 0;
border-radius: 999px;
padding: 0.8rem 1.15rem;
border-radius: 12px;
padding: 0.78rem 1.05rem;
background: linear-gradient(135deg, var(--brand) 0%, var(--brand-strong) 100%);
color: #fff;
font: inherit;
@@ -312,11 +359,11 @@
.hero {
display: grid;
gap: 24px;
gap: 16px;
margin-bottom: 28px;
padding: 30px;
padding: 22px;
border: 1px solid var(--line);
border-radius: var(--radius-xl);
border-radius: 22px;
background:
linear-gradient(135deg, rgba(255, 251, 244, 0.98), rgba(252, 247, 240, 0.95));
box-shadow: var(--shadow);
@@ -368,10 +415,10 @@
.form-panel,
.table-card {
border: 1px solid var(--line);
border-radius: var(--radius-lg);
border-radius: 16px;
background: var(--bg-elevated);
box-shadow: var(--shadow);
padding: 22px;
padding: 18px;
}
.card__eyebrow {
@@ -403,8 +450,8 @@
display: flex;
gap: 12px;
align-items: flex-start;
padding: 14px 16px;
border-radius: 16px;
padding: 12px 14px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(31, 41, 51, 0.08);
}
@@ -434,8 +481,8 @@
.split {
display: grid;
gap: 18px;
grid-template-columns: minmax(0, 1.4fr) minmax(0, 0.9fr);
gap: 14px;
grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.95fr);
}
.auth-summary__card {
@@ -452,8 +499,8 @@
justify-content: space-between;
gap: 12px;
align-items: center;
padding: 14px 16px;
border-radius: 16px;
padding: 12px 14px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.82);
border: 1px solid rgba(31, 41, 51, 0.08);
}
@@ -463,10 +510,10 @@
.tenant-row__copy { margin: 0; color: var(--muted); font-size: 0.92rem; }
.callout {
padding: 16px 18px;
border-radius: 18px;
background: rgba(45, 106, 79, 0.08);
border: 1px solid rgba(45, 106, 79, 0.14);
padding: 14px 16px;
border-radius: 16px;
background: rgba(45, 106, 79, 0.06);
border: 1px solid rgba(45, 106, 79, 0.12);
color: var(--brand-strong);
}
@@ -588,18 +635,229 @@
font-size: 0.92rem;
}
.marketing-shell {
min-height: 100vh;
padding: 0 0 92px;
gap: 12px;
}
.marketing-main {
width: min(1240px, calc(100% - 48px));
margin: 0 auto;
}
.marketing-bar {
position: sticky;
top: 0;
z-index: 30;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
height: 88px;
min-height: 88px;
width: 100%;
margin: 0 0 20px;
padding: 0 18px;
border: 0;
border-bottom: 1px solid rgba(137, 154, 188, 0.18);
border-radius: 0;
background: rgba(8, 10, 18, 0.82);
box-shadow: none;
backdrop-filter: blur(16px);
}
.marketing-nav {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.marketing-bar .brand {
gap: 8px;
flex: 0 0 auto;
}
.marketing-bar .brand__mark {
width: 40px;
height: 40px;
border-radius: 10px;
font-size: 1rem;
box-shadow: none;
}
.marketing-bar .brand__text {
gap: 1px;
}
.marketing-bar .brand__title {
font-size: 1.08rem;
line-height: 1;
}
.marketing-bar .brand__subtitle {
display: none;
}
.marketing-nav__link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
padding: 0 0.3rem;
border-radius: 6px;
border: 1px solid transparent;
color: rgba(231, 238, 255, 0.8);
font-size: 1rem;
font-weight: 600;
}
.marketing-nav__link:hover {
color: #f4f7ff;
text-decoration: none;
background: rgba(255, 255, 255, 0.06);
border-color: rgba(201, 214, 255, 0.12);
}
.marketing-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
.marketing-actions .button {
min-height: 38px;
padding: 0.42rem 0.8rem;
border-radius: 8px;
font-size: 0.94rem;
}
.marketing-actions .button--ghost,
.marketing-mobile__footer .button--ghost {
color: #f4f7ff;
border-color: rgba(201, 214, 255, 0.16);
}
.marketing-mobile {
display: none;
position: relative;
}
.marketing-mobile[open] { z-index: 40; }
.marketing-mobile__toggle {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
list-style: none;
cursor: pointer;
min-width: 38px;
min-height: 38px;
padding: 0.24rem 0.46rem;
border-radius: 8px;
border: 1px solid rgba(201, 214, 255, 0.14);
background: rgba(255, 255, 255, 0.04);
color: #f4f7ff;
}
.marketing-mobile__toggle::-webkit-details-marker { display: none; }
.marketing-mobile__toggle::before {
content: "";
width: 16px;
height: 10px;
border-top: 2px solid currentColor;
border-bottom: 2px solid currentColor;
box-shadow: inset 0 -4px 0 0 currentColor;
}
.marketing-mobile__panel {
position: absolute;
right: 0;
top: calc(100% + 10px);
width: min(280px, calc(100vw - 24px));
padding: 14px;
border-radius: 16px;
border: 1px solid rgba(163, 183, 255, 0.14);
background: rgba(8, 10, 18, 0.96);
box-shadow: 0 18px 36px rgba(1, 5, 13, 0.45);
}
.marketing-mobile__stack {
display: grid;
gap: 8px;
}
.marketing-mobile__stack .marketing-nav__link {
justify-content: flex-start;
padding: 0.55rem 0.7rem;
background: rgba(255, 255, 255, 0.04);
border-color: rgba(201, 214, 255, 0.1);
}
.marketing-mobile__footer {
margin-top: 12px;
display: flex;
justify-content: flex-end;
}
.marketing-main {
display: grid;
gap: 18px;
}
.marketing-footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 25;
margin-top: 0;
padding: 10px 24px;
border-top: 1px solid rgba(137, 154, 188, 0.14);
background: rgba(8, 10, 18, 0.88);
backdrop-filter: blur(18px);
color: rgba(209, 217, 235, 0.68);
font-size: 0.92rem;
}
.marketing-footer__inner {
display: flex;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
width: min(1240px, calc(100% - 48px));
margin: 0 auto;
}
@media (max-width: 980px) {
.site-shell {
padding: 10px 0 18px;
}
.site-header,
.site-main,
.app-footer {
width: calc(100% - 20px);
}
.site-header__inner {
padding: 14px 16px;
align-items: flex-start;
}
.top-nav { display: none; }
.app-shell {
width: calc(100% - 20px);
margin: 10px auto;
min-height: calc(100vh - 20px);
grid-template-columns: 1fr;
}
.app-sidebar { display: none; }
.mobile-nav { display: block; }
.context-bar { padding-top: 0; }
.context-bar__inner { padding: 18px; }
.grid--2,
.grid--3,
.grid--4,
@@ -608,99 +866,133 @@
.hero,
.card,
.panel,
.form-panel { padding: 18px; }
.form-panel { padding: 16px; }
.table-card { padding: 0; }
table { min-width: 0; }
.marketing-shell { padding: 0 0 88px; }
.marketing-main {
width: calc(100% - 24px);
}
.marketing-bar {
height: 72px;
min-height: 72px;
margin: 0 0 16px;
padding: 0 12px;
}
.marketing-nav,
.marketing-actions { display: none; }
.marketing-mobile { display: block; }
.marketing-footer {
padding: 10px 12px;
}
.marketing-footer__inner {
width: 100%;
}
}
@media (max-width: 980px) {
.app-body { gap: 14px; }
}
</style>
@yield('page_styles')
</head>
<body class="@yield('body_class')">
<div class="app-shell">
<aside class="app-sidebar" aria-label="Tenant-Navigation">
<div class="app-sidebar__panel">
@if ($resolvedLayoutMode === 'marketing')
<div class="marketing-shell">
<header class="marketing-bar">
<div class="brand">
<div class="brand__mark">K</div>
<div class="brand__text">
<h1 class="brand__title">Die Kaffeeliste</h1>
<p class="brand__subtitle">Kaffeekasse und Verwaltung im Tenant</p>
</div>
</div>
<nav class="sidebar-nav" aria-label="Hauptnavigation">
@forelse ($layoutNavItems as $item)
<nav class="marketing-nav" aria-label="Marketing-Navigation">
<a href="/" class="marketing-nav__link">Start</a>
<a href="/login/" class="marketing-nav__link">Anmeldung</a>
<a href="/admin/login/" class="marketing-nav__link">Admin</a>
</nav>
<div class="marketing-actions">
<a class="button button--ghost" href="/login/">Anmelden</a>
</div>
<details class="marketing-mobile">
<summary class="marketing-mobile__toggle" aria-label="Menü"></summary>
<div class="marketing-mobile__panel">
<nav class="marketing-mobile__stack" aria-label="Mobile Marketing-Navigation">
<a href="/" class="marketing-nav__link">Start</a>
<a href="/login/" class="marketing-nav__link">Anmeldung</a>
<a href="/admin/login/" class="marketing-nav__link">Admin</a>
</nav>
<div class="marketing-mobile__footer">
<a class="button button--ghost" href="/login/">Anmelden</a>
</div>
</div>
</details>
</header>
<main class="marketing-main">
@yield('content')
</main>
<footer class="marketing-footer">
<div class="marketing-footer__inner">
<span>Die Kaffeeliste</span>
<span>Ein ruhiger Einstieg für Teams</span>
</div>
</footer>
</div>
@else
<div class="site-shell">
<header class="site-header">
<div class="site-header__inner">
<a href="{{ is_array($layoutAuth) ? '/dashboard/' : '/' }}" class="brand brand--link">
<div class="brand__mark">K</div>
<div class="brand__text">
<h1 class="brand__title">Die Kaffeeliste</h1>
<p class="brand__subtitle">
@if (is_array($layoutAuth))
{{ $layoutCurrentLabel }}
@else
Für Teams gemacht.
@endif
</p>
</div>
</a>
<nav class="top-nav" aria-label="Hauptnavigation">
@foreach ($layoutPrimaryNavItems as $item)
@php
$itemHref = rtrim((string) ($item['href'] ?? '/'), '/');
$itemHref = $itemHref === '' ? '/' : $itemHref;
$isActive = $layoutPath === $itemHref;
@endphp
<a href="{{ $item['href'] ?? '/' }}" class="sidebar-nav__link {{ $isActive ? 'is-active' : '' }}" @if ($isActive) aria-current="page" @endif>{{ $item['label'] ?? 'Link' }}</a>
@empty
@php
$guestItems = [
['href' => '/', 'label' => 'Start'],
['href' => '/login/', 'label' => 'Anmeldung'],
];
@endphp
@foreach ($guestItems as $item)
@php
$itemHref = rtrim((string) ($item['href'] ?? '/'), '/');
$itemHref = $itemHref === '' ? '/' : $itemHref;
$isActive = $layoutPath === $itemHref;
@endphp
<a href="{{ $item['href'] }}" class="sidebar-nav__link {{ $isActive ? 'is-active' : '' }}" @if ($isActive) aria-current="page" @endif>{{ $item['label'] }}</a>
@endforeach
@endforelse
<a href="{{ $item['href'] ?? '/' }}" class="top-nav__link {{ $isActive ? 'is-active' : '' }}" @if ($isActive) aria-current="page" @endif>{{ $item['label'] ?? 'Link' }}</a>
@endforeach
</nav>
<div class="sidebar-footer">
<div class="sidebar-note">
<strong>Menü statt Sprungboxen</strong>
Bereiche werden über die Navigation geöffnet und nicht mehr im Inhalt verlinkt.
</div>
<div class="site-actions">
@if (is_array($layoutAuth))
<span class="pill">{{ $layoutAuth['tenant_name'] ?? 'Tenant' }}</span>
<form method="post" action="/logout/">
<button type="submit" class="button button--ghost" style="width: 100%;">Abmelden</button>
<button type="submit" class="button button--ghost">Abmelden</button>
</form>
@else
<a class="button button--ghost" href="/login/">Anmelden</a>
@endif
</div>
</div>
</aside>
<div class="app-body">
<section class="context-bar">
<div class="context-bar__inner">
<div class="context-copy">
<h2 class="context-copy__title">Die Kaffeeliste</h2>
<p class="context-copy__lead">
@if (is_array($layoutAuth))
Ruhige Tenant-Oberfläche mit klarer Navigation und ohne doppelte Statusinfos.
@else
Anmeldung, Tenant-Zugriff und Verwaltung bleiben in einer ruhigen gemeinsamen Struktur.
@endif
</p>
@if (!is_array($layoutAuth))
<div class="header-meta">
<span class="badge">Mandantenfähig</span>
<span class="badge">Mobil tauglich</span>
</div>
@endif
</div>
<details class="mobile-nav">
<summary class="mobile-nav__toggle">Menü</summary>
<div class="mobile-nav__panel">
<nav class="mobile-nav__stack" aria-label="Mobile Hauptnavigation">
@forelse ($layoutNavItems as $item)
@foreach ($layoutPrimaryNavItems as $item)
@php
$itemHref = rtrim((string) ($item['href'] ?? '/'), '/');
$itemHref = $itemHref === '' ? '/' : $itemHref;
$isActive = $layoutPath === $itemHref;
@endphp
<a href="{{ $item['href'] ?? '/' }}" class="sidebar-nav__link {{ $isActive ? 'is-active' : '' }}" @if ($isActive) aria-current="page" @endif>{{ $item['label'] ?? 'Link' }}</a>
@empty
<a href="/" class="sidebar-nav__link {{ $layoutPath === '/' ? 'is-active' : '' }}">Start</a>
<a href="/login/" class="sidebar-nav__link {{ $layoutPath === '/login' ? 'is-active' : '' }}">Anmeldung</a>
@endforelse
<a href="{{ $item['href'] ?? '/' }}" class="top-nav__link {{ $isActive ? 'is-active' : '' }}" @if ($isActive) aria-current="page" @endif>{{ $item['label'] ?? 'Link' }}</a>
@endforeach
</nav>
@if (is_array($layoutAuth))
<div class="mobile-nav__footer">
@@ -712,19 +1004,20 @@
</div>
</details>
</div>
</section>
</div>
</header>
<main class="app-main">
<main class="site-main app-main">
@yield('content')
</main>
<footer class="app-footer">
<div class="app-footer__inner">
<span>Die Kaffeeliste</span>
<span>Mitglieder, Striche, Einzahlungen, Hinweise und Exporte in einem System</span>
<span>Klare Bereiche mit Navigation oben</span>
</div>
</footer>
</div>
</div>
@endif
</body>
</html>
+91 -110
View File
@@ -46,82 +46,61 @@
width:min(1460px,calc(100vw - 32px));
margin:20px auto 40px;
display:grid;
grid-template-columns:minmax(280px,300px) minmax(0,1fr);
gap:20px;
align-items:start;
}
.sidebar,.hero,.card,.alert{
.site-header{position:sticky;top:20px;z-index:20}
.site-header__inner{
display:flex;
align-items:center;
justify-content:space-between;
gap:18px;
padding:16px 18px;
border:1px solid var(--line);
border-radius:24px;
background:rgba(255,251,244,.96);
box-shadow:var(--shadow);
}
.site-brand{display:flex;align-items:center;gap:14px;min-width:0}
.site-brand__mark{
width:44px;
height:44px;
border-radius:16px;
display:grid;
place-items:center;
background:linear-gradient(135deg,var(--brand) 0%,var(--brand-strong) 100%);
color:#fff;
font-weight:800;
box-shadow:0 14px 28px rgba(var(--brand-rgb),.18);
}
.site-brand__title{margin:0;font-family:Georgia,serif;font-size:1.1rem;letter-spacing:-.02em}
.site-brand__subtitle{margin:2px 0 0;color:var(--muted);font-size:.92rem}
.site-nav,.site-actions,.actions,.context{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.site-nav__link{
display:inline-flex;
align-items:center;
justify-content:center;
padding:10px 14px;
border-radius:999px;
border:1px solid transparent;
color:var(--muted);
font-weight:700;
}
.site-nav__link:hover{text-decoration:none;color:var(--brand);background:rgba(255,255,255,.82);border-color:rgba(var(--brand-rgb),.14)}
.site-nav__link.active{background:rgba(var(--brand-rgb),.10);color:var(--brand);border-color:rgba(var(--brand-rgb),.18)}
.hero,.card,.alert{
border:1px solid var(--line);
border-radius:var(--radius);
background:var(--card);
box-shadow:var(--shadow);
}
.sidebar{
position:sticky;
top:20px;
display:grid;
gap:16px;
padding:18px;
background:rgba(255,251,244,.96);
}
.sidebar__brand{
display:grid;
gap:6px;
padding-bottom:14px;
border-bottom:1px solid rgba(37,24,15,.1);
}
.sidebar__eyebrow{
margin:0;
color:var(--accent);
text-transform:uppercase;
letter-spacing:.16em;
font-size:.78rem;
font-weight:800;
}
.sidebar__title,.hero__title,.card h2,.card h3{
.hero__title,.card h2,.card h3{
font-family:Georgia,serif;
letter-spacing:-.02em;
}
.sidebar__title{
margin:0;
font-size:1.55rem;
line-height:1.05;
}
.sidebar__subtitle,.muted,p{color:var(--muted)}
.sidebar__meta,.actions,.context{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.sidebar__section{
display:inline-block;
color:var(--accent);
text-transform:uppercase;
letter-spacing:.14em;
font-size:.76rem;
font-weight:800;
}
.sidebar__nav{display:grid;gap:10px}
.sidebar__link{
display:flex;
align-items:center;
justify-content:space-between;
padding:12px 14px;
border-radius:16px;
border:1px solid rgba(var(--brand-rgb),.12);
background:#fff;
color:var(--brand);
font-weight:700;
}
.sidebar__link.active{
background:var(--brand);
color:#fff;
}
.sidebar__footer{
display:grid;
gap:12px;
padding-top:14px;
border-top:1px solid rgba(37,24,15,.1);
}
.sidebar__mobile{display:none}
.sidebar__mobile[open]{z-index:20}
.sidebar__toggle{
.muted,p{color:var(--muted)}
.site-mobile{display:none;position:relative}
.site-mobile[open]{z-index:20}
.site-toggle{
display:flex;
align-items:center;
justify-content:space-between;
@@ -135,8 +114,8 @@
color:var(--brand);
font-weight:700;
}
.sidebar__toggle::-webkit-details-marker{display:none}
.sidebar__toggle::after{
.site-toggle::-webkit-details-marker{display:none}
.site-toggle::after{
content:"";
width:11px;
height:11px;
@@ -145,16 +124,21 @@
transform:rotate(45deg);
transition:transform .2s ease;
}
.sidebar__mobile[open] .sidebar__toggle::after{transform:rotate(225deg)}
.sidebar__panel{
margin-top:12px;
.site-mobile[open] .site-toggle::after{transform:rotate(225deg)}
.site-panel{
position:absolute;
right:0;
top:calc(100% + 12px);
width:min(320px,calc(100vw - 32px));
padding:14px;
border-radius:18px;
border:1px solid var(--line);
background:#fffdf9;
box-shadow:var(--shadow);
}
.sidebar__stack{display:grid;gap:10px}
.site-stack{display:grid;gap:10px}
.site-stack .site-nav__link{justify-content:flex-start;background:rgba(255,255,255,.78);border-color:rgba(37,24,15,.08);color:var(--ink)}
.site-footer-actions{display:flex;justify-content:flex-end;margin-top:12px}
.content{min-width:0;display:grid;gap:18px}
.hero{
padding:24px;
@@ -253,59 +237,56 @@
.mono{font-family:Consolas,monospace}
.footer{margin-top:18px;text-align:center;color:var(--muted);font-size:.92rem}
@media(max-width:1040px){
.page-shell{grid-template-columns:1fr;width:min(100vw - 20px,1460px)}
.sidebar{position:static}
.sidebar__desktop{display:none}
.sidebar__mobile{display:block}
.page-shell{width:min(100vw - 20px,1460px)}
.site-header__inner{align-items:flex-start;padding:14px 16px}
.site-nav{display:none}
.site-mobile{display:block}
.split,.grid--2,.grid--3,.grid--4{grid-template-columns:1fr}
table{min-width:0}
}
@media(min-width:1041px){
.sidebar__mobile{display:none}
.site-mobile{display:none}
}
</style>
</head>
<body>
<main class="page-shell">
<aside class="sidebar" aria-label="Bereichsnavigation">
<div class="sidebar__brand">
<p class="sidebar__eyebrow">Die Kaffeeliste</p>
<h1 class="sidebar__title">Support</h1>
<p class="sidebar__subtitle">Support, Vorgänge und Rückmeldungen im Tenant.</p>
</div>
<header class="site-header">
<div class="site-header__inner">
<a href="/dashboard/" class="site-brand">
<div class="site-brand__mark">K</div>
<div>
<h1 class="site-brand__title">Die Kaffeeliste</h1>
<p class="site-brand__subtitle">Support</p>
</div>
</a>
<div class="sidebar__meta">
<?= support_badge($isManager ? 'Verantwortlichen-Sicht' : 'Mitgliedersicht', 'success') ?>
<?= support_badge('Tenant-weit') ?>
<?= support_badge('Status und Routing', 'warning') ?>
</div>
<div class="sidebar__desktop">
<span class="sidebar__section">Bereiche</span>
<nav class="sidebar__nav" aria-label="Tenant-Menü">
<nav class="site-nav" aria-label="Tenant-Menü">
<?php foreach ($tenantNavItems as $item): ?>
<a href="<?= support_h((string) ($item['href'] ?? '/')) ?>" class="sidebar__link <?= (($item['key'] ?? '') === 'support') ? 'active' : '' ?>" <?= (($item['key'] ?? '') === 'support') ? 'aria-current="page"' : '' ?>><?= support_h((string) ($item['label'] ?? 'Link')) ?></a>
<a href="<?= support_h((string) ($item['href'] ?? '/')) ?>" class="site-nav__link <?= (($item['key'] ?? '') === 'support') ? 'active' : '' ?>" <?= (($item['key'] ?? '') === 'support') ? 'aria-current="page"' : '' ?>><?= support_h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
</nav>
<div class="sidebar__footer">
<div class="site-actions">
<?= support_badge($isManager ? 'Verantwortlichen-Sicht' : 'Mitgliedersicht', 'success') ?>
<form method="post" action="/logout/"><button type="submit" class="button button--ghost">Abmelden</button></form>
<details class="site-mobile">
<summary class="site-toggle">Menue</summary>
<div class="site-panel">
<nav class="site-stack" aria-label="Mobiles Tenant-Menü">
<?php foreach ($tenantNavItems as $item): ?>
<a href="<?= support_h((string) ($item['href'] ?? '/')) ?>" class="site-nav__link <?= (($item['key'] ?? '') === 'support') ? 'active' : '' ?>" <?= (($item['key'] ?? '') === 'support') ? 'aria-current="page"' : '' ?>><?= support_h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
</nav>
<div class="site-footer-actions">
<form method="post" action="/logout/"><button type="submit" class="button button--ghost">Abmelden</button></form>
</div>
</div>
</details>
</div>
</div>
<details class="sidebar__mobile">
<summary class="sidebar__toggle">Menü</summary>
<div class="sidebar__panel">
<nav class="sidebar__stack" aria-label="Mobiles Tenant-Menü">
<?php foreach ($tenantNavItems as $item): ?>
<a href="<?= support_h((string) ($item['href'] ?? '/')) ?>" class="sidebar__link <?= (($item['key'] ?? '') === 'support') ? 'active' : '' ?>" <?= (($item['key'] ?? '') === 'support') ? 'aria-current="page"' : '' ?>><?= support_h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
</nav>
<div class="sidebar__footer">
<form method="post" action="/logout/"><button type="submit" class="button button--ghost">Abmelden</button></form>
</div>
</div>
</details>
</aside>
</header>
<div class="content">
<section class="hero">
@@ -351,7 +332,7 @@
<label>Betreff<input name="subject" maxlength="255" placeholder="Worum geht es?" required></label>
<label>Kategorie
<select name="category">
<?php foreach ($categories as $category): ?>
<?php foreach ($tenantNavItems as $item): ?>
<option value="<?= support_h($category) ?>"><?= support_h(ucfirst(str_replace('_', ' ', $category))) ?></option>
<?php endforeach; ?>
</select>
+94 -105
View File
@@ -47,76 +47,50 @@
width:min(1460px,calc(100vw - 32px));
margin:20px auto 40px;
display:grid;
grid-template-columns:minmax(280px,300px) minmax(0,1fr);
gap:20px;
align-items:start;
}
.sidebar,.hero,.card,.table-card,.note{
border:1px solid var(--line);
border-radius:var(--radius);
background:var(--card);
box-shadow:var(--shadow);
}
.sidebar{
position:sticky;
top:20px;
display:grid;
gap:16px;
padding:18px;
background:rgba(255,251,244,.96);
}
.sidebar__brand{
display:grid;
gap:6px;
padding-bottom:14px;
border-bottom:1px solid rgba(37,24,15,.1);
}
.sidebar__eyebrow{
margin:0;
color:var(--accent);
text-transform:uppercase;
letter-spacing:.16em;
font-size:.78rem;
font-weight:800;
}
.sidebar__title,.hero__title,.card h2,.card h3{font-family:Georgia,serif;letter-spacing:-.02em}
.sidebar__title{
margin:0;
font-size:1.55rem;
line-height:1.05;
}
.sidebar__subtitle,.muted{color:var(--muted)}
.sidebar__meta,.actions,.context,.meta{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.sidebar__section{
display:inline-block;
color:var(--accent);
text-transform:uppercase;
letter-spacing:.14em;
font-size:.76rem;
font-weight:800;
}
.sidebar__nav{display:grid;gap:10px}
.sidebar__link{
.site-header{position:sticky;top:20px;z-index:20}
.site-header__inner{
display:flex;
align-items:center;
justify-content:space-between;
padding:12px 14px;
gap:18px;
padding:16px 18px;
border:1px solid var(--line);
border-radius:24px;
background:rgba(255,251,244,.96);
box-shadow:var(--shadow);
}
.site-brand{display:flex;align-items:center;gap:14px;min-width:0}
.site-brand__mark{
width:44px;
height:44px;
border-radius:16px;
border:1px solid rgba(var(--brand-rgb),.12);
background:#fff;
color:var(--brand);
display:grid;
place-items:center;
background:linear-gradient(135deg,var(--brand) 0%,var(--brand-strong) 100%);
color:#fff;
font-weight:800;
box-shadow:0 14px 28px rgba(var(--brand-rgb),.18);
}
.site-brand__title{margin:0;font-size:1.1rem}
.site-brand__subtitle{margin:2px 0 0;color:var(--muted);font-size:.92rem}
.site-nav,.site-actions,.actions,.context,.meta{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.site-nav__link{
display:inline-flex;
align-items:center;
justify-content:center;
padding:10px 14px;
border-radius:999px;
border:1px solid transparent;
color:var(--muted);
font-weight:700;
}
.sidebar__link.active{background:var(--brand);color:#fff}
.sidebar__footer{
display:grid;
gap:12px;
padding-top:14px;
border-top:1px solid rgba(37,24,15,.1);
}
.sidebar__mobile{display:none}
.sidebar__mobile[open]{z-index:20}
.sidebar__toggle{
.site-nav__link:hover{text-decoration:none;color:var(--brand);background:rgba(255,255,255,.82);border-color:rgba(var(--brand-rgb),.14)}
.site-nav__link.active{background:rgba(var(--brand-rgb),.10);color:var(--brand);border-color:rgba(var(--brand-rgb),.18)}
.site-mobile{display:none;position:relative}
.site-mobile[open]{z-index:20}
.site-toggle{
display:flex;
align-items:center;
justify-content:space-between;
@@ -130,8 +104,8 @@
color:var(--brand);
font-weight:700;
}
.sidebar__toggle::-webkit-details-marker{display:none}
.sidebar__toggle::after{
.site-toggle::-webkit-details-marker{display:none}
.site-toggle::after{
content:"";
width:11px;
height:11px;
@@ -140,16 +114,34 @@
transform:rotate(45deg);
transition:transform .2s ease;
}
.sidebar__mobile[open] .sidebar__toggle::after{transform:rotate(225deg)}
.sidebar__panel{
margin-top:12px;
.site-mobile[open] .site-toggle::after{transform:rotate(225deg)}
.site-panel{
position:absolute;
right:0;
top:calc(100% + 12px);
width:min(320px,calc(100vw - 32px));
padding:14px;
border-radius:18px;
border:1px solid var(--line);
background:#fffdf9;
box-shadow:var(--shadow);
}
.sidebar__stack{display:grid;gap:10px}
.site-stack{display:grid;gap:10px}
.site-stack .site-nav__link{
justify-content:flex-start;
background:rgba(255,255,255,.78);
border-color:rgba(37,24,15,.08);
color:var(--ink);
}
.site-footer-actions{display:flex;justify-content:flex-end;margin-top:12px}
.hero,.card,.table-card,.note{
border:1px solid var(--line);
border-radius:var(--radius);
background:var(--card);
box-shadow:var(--shadow);
}
.hero__title,.card h2,.card h3{font-family:Georgia,serif;letter-spacing:-.02em}
.sidebar__subtitle,.muted{color:var(--muted)}
.content{min-width:0;display:grid;gap:18px}
.hero{
display:grid;
@@ -216,58 +208,55 @@
white-space:nowrap;
}
@media(max-width:960px){
.page-shell{grid-template-columns:1fr;width:min(100vw - 20px,1460px)}
.sidebar{position:static}
.sidebar__desktop{display:none}
.sidebar__mobile{display:block}
.page-shell{width:min(100vw - 20px,1460px)}
.site-header__inner{align-items:flex-start;padding:14px 16px}
.site-nav{display:none}
.site-mobile{display:block}
.hero,.grid--2,.grid--3,.grid--4{grid-template-columns:1fr}
}
@media(min-width:961px){
.sidebar__mobile{display:none}
.site-mobile{display:none}
}
</style>
</head>
<body>
<main class="page-shell">
<aside class="sidebar" aria-label="Bereichsnavigation">
<div class="sidebar__brand">
<p class="sidebar__eyebrow">Die Kaffeeliste</p>
<h1 class="sidebar__title">Rollen</h1>
<p class="sidebar__subtitle">Rollen und Rechte im Tenant.</p>
</div>
<header class="site-header">
<div class="site-header__inner">
<a href="/dashboard/" class="site-brand">
<div class="site-brand__mark">K</div>
<div>
<h1 class="site-brand__title">Die Kaffeeliste</h1>
<p class="site-brand__subtitle">Rollen</p>
</div>
</a>
<div class="sidebar__meta">
<span class="badge">lokal + ADFS/OIDC</span>
<span class="badge">Rollenmatrix</span>
<span class="badge">Delegation</span>
</div>
<div class="sidebar__desktop">
<span class="sidebar__section">Bereiche</span>
<nav class="sidebar__nav" aria-label="Tenant-Menü">
<nav class="site-nav" aria-label="Tenant-Menü">
<?php foreach ($tenantNavItems as $item): ?>
<a class="sidebar__link <?= (($item['key'] ?? '') === 'roles') ? 'active' : '' ?>" href="<?= tenant_roles_h((string) ($item['href'] ?? '/')) ?>" <?= (($item['key'] ?? '') === 'roles') ? 'aria-current="page"' : '' ?>><?= tenant_roles_h((string) ($item['label'] ?? 'Link')) ?></a>
<a class="site-nav__link <?= (($item['key'] ?? '') === 'roles') ? 'active' : '' ?>" href="<?= tenant_roles_h((string) ($item['href'] ?? '/')) ?>" <?= (($item['key'] ?? '') === 'roles') ? 'aria-current="page"' : '' ?>><?= tenant_roles_h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
</nav>
<div class="sidebar__footer">
<div class="site-actions">
<span class="pill">Rollenmatrix</span>
<form method="post" action="/logout/"><button type="submit" class="button button--ghost">Abmelden</button></form>
<details class="site-mobile">
<summary class="site-toggle">Menue</summary>
<div class="site-panel">
<nav class="site-stack" aria-label="Mobiles Tenant-Menü">
<?php foreach ($tenantNavItems as $item): ?>
<a class="site-nav__link <?= (($item['key'] ?? '') === 'roles') ? 'active' : '' ?>" href="<?= tenant_roles_h((string) ($item['href'] ?? '/')) ?>" <?= (($item['key'] ?? '') === 'roles') ? 'aria-current="page"' : '' ?>><?= tenant_roles_h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
</nav>
<div class="site-footer-actions">
<form method="post" action="/logout/"><button type="submit" class="button button--ghost">Abmelden</button></form>
</div>
</div>
</details>
</div>
</div>
<details class="sidebar__mobile">
<summary class="sidebar__toggle">Menü</summary>
<div class="sidebar__panel">
<nav class="sidebar__stack" aria-label="Mobiles Tenant-Menü">
<?php foreach ($tenantNavItems as $item): ?>
<a class="sidebar__link <?= (($item['key'] ?? '') === 'roles') ? 'active' : '' ?>" href="<?= tenant_roles_h((string) ($item['href'] ?? '/')) ?>" <?= (($item['key'] ?? '') === 'roles') ? 'aria-current="page"' : '' ?>><?= tenant_roles_h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
</nav>
<div class="sidebar__footer">
<form method="post" action="/logout/"><button type="submit" class="button button--ghost">Abmelden</button></form>
</div>
</div>
</details>
</aside>
</header>
<div class="content">
<section class="hero">
+286 -154
View File
@@ -1,176 +1,308 @@
@extends('layouts.app')
@section('page_title', 'Die Kaffeeliste - Zentrale Plattform')
@section('layout_mode', 'marketing')
@section('page_title', 'Die Kaffeeliste')
@section('body_class', 'landing-preview')
@php
$overview = $tenantOverview ?? [
'metrics' => [
['label' => 'Aktive Tenants', 'value' => '4', 'detail' => 'Mandanten ueber die Plattform verteilt.'],
['label' => 'Mitglieder gesamt', 'value' => '100', 'detail' => 'Aktive Nutzerkonten im Verbund.'],
['label' => 'SSO-Abdeckung', 'value' => '4', 'detail' => 'Tenant-Logins mit zentraler Identitaetsstrategie.'],
['label' => 'Betriebsstatus', 'value' => 'Stabil', 'detail' => 'Queues und Exporte fuer den Webspace-Betrieb vorbereitet.'],
$columns = $landingColumns ?? [
[
'eyebrow' => 'Für Mitglieder',
'title' => 'Schnell starten',
'copy' => 'Einloggen und direkt weiter.',
],
[
'eyebrow' => 'Für Verantwortliche',
'title' => 'Alles im Blick',
'copy' => 'Bereiche, Hinweise und Verwaltung an einem Ort.',
],
[
'eyebrow' => 'Für Standorte',
'title' => 'Gemeinsam organisiert',
'copy' => 'Ein klarer Ablauf für Teams und Bereiche.',
],
'tenants' => [],
];
$preview = $centralLoginPreview ?? [
'single' => ['email' => 'mia@berlin.example', 'matches' => [['name' => 'Werk Berlin', 'domain' => 'berlin.kaffeeliste.de']]],
'multiple' => ['email' => 'leitung@kaffeeliste.example', 'matches' => [['name' => 'Werk Berlin', 'domain' => 'berlin.kaffeeliste.de'], ['name' => 'Werk Koeln', 'domain' => 'koeln.kaffeeliste.de']]],
'unknown' => ['email' => 'extern@example.org', 'matches' => []],
$preview = $landingPreview ?? [
['label' => 'Start', 'value' => 'Klar'],
['label' => 'Team', 'value' => 'Gemeinsam'],
['label' => 'Zugang', 'value' => 'Direkt'],
];
@endphp
@section('page_styles')
<style>
body.landing-preview {
background:
radial-gradient(circle at top center, rgba(88, 122, 255, 0.22), transparent 28%),
radial-gradient(circle at 20% 20%, rgba(42, 72, 170, 0.18), transparent 24%),
linear-gradient(180deg, #05070d 0%, #0a0f1a 52%, #0d1320 100%);
color: #f4f7ff;
}
body.landing-preview h1,
body.landing-preview h2,
body.landing-preview h3 {
font-family: "Inter Tight", "Aptos", "Segoe UI", sans-serif;
letter-spacing: -0.04em;
}
.landing-wrap {
display: grid;
gap: 56px;
padding: 8px 0 12px;
}
.hero {
display: grid;
grid-template-columns: minmax(0, 1.08fr) minmax(280px, 0.92fr);
gap: 40px;
align-items: start;
padding-top: 12px;
}
.kicker {
margin: 0 0 10px;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.16em;
color: #8aa0d0;
font-weight: 700;
}
.hero h1 {
margin: 0;
font-size: clamp(3.2rem, 7vw, 6.4rem);
line-height: 0.9;
}
.hero p {
margin: 14px 0 0;
max-width: 30rem;
font-size: 1.05rem;
line-height: 1.65;
color: rgba(219, 228, 248, 0.72);
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 24px;
}
.landing-preview,
.landing-callout {
border: 1px solid rgba(163, 183, 255, 0.12);
border-radius: 20px;
background: rgba(14, 20, 34, 0.86);
box-shadow: 0 16px 40px rgba(2, 5, 12, 0.34);
}
.landing-preview {
padding: 20px;
display: grid;
gap: 12px;
}
.landing-preview__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(163, 183, 255, 0.12);
}
.landing-preview__title {
margin: 0;
font-size: 1.1rem;
color: #f4f7ff;
}
.landing-preview__line {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
padding: 12px 14px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.04);
color: rgba(216, 225, 246, 0.72);
font-weight: 600;
}
.landing-preview__line strong {
color: #f4f7ff;
}
.landing-columns {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0;
border-top: 1px solid rgba(163, 183, 255, 0.12);
border-bottom: 1px solid rgba(163, 183, 255, 0.12);
}
.landing-column {
padding: 24px 18px;
}
.landing-column + .landing-column {
border-left: 1px solid rgba(163, 183, 255, 0.12);
}
.landing-column__eyebrow {
margin: 0 0 10px;
color: #8aa0d0;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.landing-column h2 {
margin: 0 0 10px;
font-size: 1.45rem;
color: #f4f7ff;
}
.landing-column p {
margin: 0;
max-width: 22rem;
color: rgba(219, 228, 248, 0.72);
}
.landing-callout {
padding: 24px 26px;
}
.landing-callout__eyebrow {
margin: 0 0 8px;
color: #8aa0d0;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.landing-callout h2 {
margin: 0 0 10px;
font-size: 2rem;
color: #f4f7ff;
}
.landing-callout p {
margin: 0;
max-width: 36rem;
color: rgba(217, 226, 246, 0.8);
}
.landing-cta {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
padding-top: 20px;
margin-top: 22px;
border-top: 1px solid rgba(163, 183, 255, 0.12);
}
.landing-cta__copy {
display: grid;
gap: 6px;
}
.landing-cta__copy strong {
font-size: 1.05rem;
color: #f4f7ff;
}
.landing-cta__copy span {
color: rgba(217, 226, 246, 0.72);
}
@media (max-width: 960px) {
.landing-wrap {
gap: 36px;
}
.hero,
.landing-columns {
grid-template-columns: 1fr;
}
.hero {
gap: 24px;
padding-top: 0;
}
.landing-column {
padding: 20px 0;
}
.landing-column + .landing-column {
border-left: 0;
border-top: 1px solid rgba(163, 183, 255, 0.12);
}
.landing-callout {
padding: 20px;
}
}
</style>
@endsection
@section('content')
<section class="hero hero--split">
<div class="hero__content">
<div class="landing-wrap">
<section class="hero">
<div>
<p class="hero__kicker">Zentrale Plattform fuer Mitglieder und Verantwortliche</p>
<h2 class="hero__title">Kaffeeliste verbindet zentrale Anmeldung, Tenant-Steuerung und den operativen Alltag in einer klaren SaaS-Oberflaeche.</h2>
<p class="hero__lead">
Endanwender finden ihren Kontostand, Striche, Einzahlungen und Hinweise ohne Tenant-Chaos. Gleichzeitig
behalten Verantwortliche alle Mandanten, Domains, SSO-Pfade und Mehrfachzuordnungen in einer gemeinsamen
Admin-Konsole im Blick.
</p>
</div>
<div class="hero__actions">
<a class="button" href="/login">Zentrale Anmeldung starten</a>
<a class="button button--ghost" href="/tenants">Tenant Console ansehen</a>
</div>
<div class="hero__meta">
<span class="badge badge--solid">Ein Login fuer alle Mitgliedschaften</span>
<span class="badge">Automatische Tenant-Weiterleitung</span>
<span class="badge">Mehrfachzuordnung mit Auswahlfluss</span>
</div>
</div>
<p class="kicker">Die Kaffeeliste</p>
<h1>Kaffee im Team. Klar organisiert.</h1>
<p>Für Mitglieder, Verantwortliche und Standorte.</p>
<aside class="hero__aside">
<article class="card metric metric--compact">
<p class="metric__label">{{ $overview['metrics'][0]['label'] }}</p>
<div class="metric__value">{{ $overview['metrics'][0]['value'] }}</div>
<p class="muted">{{ $overview['metrics'][0]['detail'] }}</p>
</article>
<article class="card metric metric--compact">
<p class="metric__label">{{ $overview['metrics'][1]['label'] }}</p>
<div class="metric__value">{{ $overview['metrics'][1]['value'] }}</div>
<p class="muted">{{ $overview['metrics'][1]['detail'] }}</p>
</article>
<div class="callout">
<strong>Zentraler Login statt Inseln</strong>
Mitglieder geben zuerst nur ihre E-Mail-Adresse an. Danach entscheidet die Plattform, ob direkt in einen Tenant
weitergeleitet wird oder ob zuerst eine Tenant-Auswahl erscheinen muss.
</div>
</aside>
</section>
<section class="grid grid--3">
<article class="card">
<p class="card__eyebrow">Fuer Mitglieder</p>
<h3>Schneller Einstieg ohne Tenant-Raten</h3>
<p class="muted">
Eine zentrale Anmeldung nimmt die Mail-Adresse entgegen und fuehrt danach automatisch in den passenden Bereich,
statt Nutzer auf Subdomains oder technische Tenant-Keys zu verweisen.
</p>
</article>
<article class="card">
<p class="card__eyebrow">Fuer Verantwortliche</p>
<h3>Alle Tenants in einer Admin-Console</h3>
<p class="muted">
Domains, SSO-Abdeckung, Rollout-Status und Mehrfachzuordnungen lassen sich tenantuebergreifend steuern und
priorisieren.
</p>
</article>
<article class="card">
<p class="card__eyebrow">Fuer den Betrieb</p>
<h3>Webspace-tauglich und tenantbewusst</h3>
<p class="muted">
Die Produktflaechen bleiben leichtgewichtig, dokumentiert und anschlussfaehig fuer Cron, Importe, Exporte und
spaetere Identity-Ausbaustufen.
</p>
</article>
</section>
<section class="split" style="margin-top: 18px;">
<article class="panel">
<p class="card__eyebrow">Login-Fluss</p>
<h3>So funktioniert die zentrale Anmeldung fuer Mitglieder.</h3>
<div class="feature-list" style="margin-top: 18px;">
<div class="feature-list__item">
<div class="feature-list__badge">1</div>
<div>
<p class="feature-list__title">E-Mail zuerst</p>
<p class="feature-list__copy">Die Plattform prueft zentral, in welchen Tenants die Mail-Adresse hinterlegt ist.</p>
</div>
</div>
<div class="feature-list__item">
<div class="feature-list__badge">2</div>
<div>
<p class="feature-list__title">Automatische Entscheidung</p>
<p class="feature-list__copy">Bei genau einer Mitgliedschaft erfolgt die Weiterleitung direkt in den korrekten Tenant.</p>
</div>
</div>
<div class="feature-list__item">
<div class="feature-list__badge">3</div>
<div>
<p class="feature-list__title">Auswahl bei Mehrfachzuordnung</p>
<p class="feature-list__copy">Bei mehreren Tenants erscheint zuerst eine Auswahl, damit Mitglieder bewusst den richtigen Kontext waehlen.</p>
</div>
<div class="actions">
<a class="button" href="/login/">Anmelden</a>
<a class="button button--ghost" href="#bereiche">Mehr erfahren</a>
</div>
</div>
</article>
<article class="panel">
<p class="card__eyebrow">Mehrfachzuordnung</p>
<h3>Beispiel fuer eine zentrale Tenant-Auswahl.</h3>
<p class="muted">
Fuer <strong>{{ $preview['multiple']['email'] }}</strong> werden mehrere Mitgliedschaften erkannt. Statt Fehlleitungen
oder separater Logins zeigt die Plattform alle erreichbaren Tenants in einer klaren Auswahl.
</p>
<div class="tenant-grid" style="margin-top: 18px;">
@foreach ($preview['multiple']['matches'] as $tenant)
<div class="tenant-row">
<div class="tenant-row__meta">
<p class="tenant-row__title">{{ $tenant['name'] }}</p>
<p class="tenant-row__copy">{{ $tenant['domain'] }}</p>
</div>
<span class="status">Auswaehlbar</span>
<aside class="landing-preview" aria-label="Vorschau">
<div class="landing-preview__header">
<h2 class="landing-preview__title">Übersicht</h2>
<span>Heute</span>
</div>
@foreach ($preview as $item)
<div class="landing-preview__line">
<span>{{ $item['label'] ?? '' }}</span>
<strong>{{ $item['value'] ?? '' }}</strong>
</div>
@endforeach
</div>
<div class="note" style="margin-top: 18px;">
Genau diese Logik wird spaeter im produktiven Login verwendet: kein technischer Tenant-Schritt fuer Mitglieder,
aber trotzdem ein sauberer Kontext fuer jede Organisation.
</div>
</article>
</section>
</aside>
</section>
<section class="grid grid--2" style="margin-top: 18px;">
<article class="panel">
<p class="card__eyebrow">Endanwender-Nutzen</p>
<h3>Was Mitglieder auf der Plattform erwarten duerfen.</h3>
<ul class="list-reset" style="margin-top: 18px;">
<li><span class="status">Kontostand</span> Ein klares Dashboard mit Verbrauch, Zahlungen und letzten Buchungen.</li>
<li><span class="status">Hinweise</span> Tenantbezogene Inhalte, FAQ und operative Informationen an einem Ort.</li>
<li><span class="status">Mitgliedschaften</span> Ein zentraler Einstieg auch fuer Personen mit mehreren Teams oder Standorten.</li>
<li><span class="status">Self-Service</span> Direkte Wege zu Ledger, Zahlungen und spaeterem Passwort-/SSO-Fallback.</li>
</ul>
</article>
<section class="landing-columns" id="bereiche" aria-label="Bereiche">
@foreach ($columns as $column)
<article class="landing-column">
<p class="landing-column__eyebrow">{{ $column['eyebrow'] ?? '' }}</p>
<h2>{{ $column['title'] ?? '' }}</h2>
<p>{{ $column['copy'] ?? '' }}</p>
</article>
@endforeach
</section>
<article class="panel">
<p class="card__eyebrow">Admin-Perspektive</p>
<h3>Was die zentrale Tenant Console sichtbar macht.</h3>
<div class="timeline timeline--tight" style="margin-top: 18px;">
<div class="timeline__item">
<p class="timeline__title">Tenant-Portfolio</p>
<p class="timeline__meta">Mitgliederzahlen, Betriebsstatus und Login-Modell aller Mandanten in einer Sicht.</p>
</div>
<div class="timeline__item">
<p class="timeline__title">Identity-Steuerung</p>
<p class="timeline__meta">SSO-Abdeckung, Fallback-Logins und tenantbezogene Einstiegspfade konsistent halten.</p>
</div>
<div class="timeline__item">
<p class="timeline__title">Rollout & Betrieb</p>
<p class="timeline__meta">Onboarding, Migrationen und operative To-dos nicht pro Tenant verstreut, sondern zentral priorisiert.</p>
<section class="landing-callout">
<p class="landing-callout__eyebrow">Einfach im Alltag</p>
<h2>Ein gemeinsamer Ort für die Kaffeeliste.</h2>
<p>Weniger suchen. Weniger erklären. Klar durch den Tag.</p>
<div class="landing-cta">
<div class="landing-cta__copy">
<strong>Bereit für den Start?</strong>
<span>Direkt zur Anmeldung.</span>
</div>
<a class="button" href="/login/">Anmelden</a>
</div>
</article>
</section>
</section>
</div>
@endsection