Anpassung Menü

This commit is contained in:
2026-04-08 23:40:47 +02:00
parent f6121b486c
commit f08b77a7fa
10 changed files with 676 additions and 276 deletions
+126 -17
View File
@@ -424,12 +424,13 @@ function app_tenant_navigation_items(?array $auth, array $license = []): array
if ($canManage) {
$items[] = ['key' => 'content', 'label' => 'Hinweise & FAQ', 'href' => '/content/'];
$items[] = ['key' => 'reports', 'label' => 'Reporting', 'href' => '/reports/'];
$items[] = ['key' => 'roles', 'label' => 'Rollen', 'href' => '/tenants/roles/'];
if (!empty($features['tenant_settings'])) {
$items[] = ['key' => 'settings', 'label' => 'Einstellungen', 'href' => '/settings/'];
$items[] = ['key' => 'settings', 'label' => 'Mandanten-Einstellungen', 'href' => '/settings/'];
}
$items[] = ['key' => 'roles', 'label' => 'Rollen', 'href' => '/tenants/roles/'];
if ($hasExports) {
$items[] = ['key' => 'exports', 'label' => 'Exporte', 'href' => '/exports/'];
}
@@ -454,7 +455,7 @@ function app_tenant_navigation_groups(array $items): array
$groupOrder = ['data', 'content', 'management'];
$groupLabels = [
'data' => 'Daten',
'content' => 'Hilfe & Inhalte',
'content' => 'Inhalte & Support',
'management' => 'Verwaltung',
];
$groupMap = [
@@ -464,6 +465,7 @@ function app_tenant_navigation_groups(array $items): array
'content' => 'content',
'support' => 'content',
'surveys' => 'content',
'members' => 'management',
'roles' => 'management',
'settings' => 'management',
];
@@ -496,6 +498,20 @@ function app_tenant_navigation_groups(array $items): array
return $result;
}
/**
* @return array<int, array{key:string,label:string,href:string}>
*/
function app_tenant_account_items(?array $auth): array
{
if (!is_array($auth)) {
return [];
}
return [
['key' => 'profile', 'label' => 'Persönliche Einstellungen', 'href' => '/profile/'],
];
}
function app_users_support_theme_mode(PDO $pdo): bool
{
static $cache = null;
@@ -1234,19 +1250,19 @@ function app_handle_profile_action(PDO $pdo, array $auth): void
if ($tenantId === '' || $tenantUserId === '') {
app_flash('Das Profil konnte nicht aktualisiert werden.', 'error');
app_redirect('/dashboard/');
app_redirect(app_request_path());
}
if ($displayName === '') {
app_flash('Bitte gib einen Anzeigenamen an.', 'error');
app_redirect('/dashboard/');
app_redirect(app_request_path());
}
$membership = app_tenant_user_by_id($pdo, $tenantUserId, $tenantId);
if ($membership === null) {
app_flash('Die zugehörige Person konnte nicht gefunden werden.', 'error');
app_redirect('/dashboard/');
app_redirect(app_request_path());
}
$now = date('Y-m-d H:i:s');
@@ -1278,7 +1294,7 @@ function app_handle_profile_action(PDO $pdo, array $auth): void
app_set_auth_user(array_merge($auth, ['display_name' => $displayName]));
app_flash('Dein Profil wurde aktualisiert.', 'success');
app_redirect('/dashboard/');
app_redirect(app_request_path());
}
function app_handle_theme_action(PDO $pdo, array $auth): void
@@ -2368,6 +2384,38 @@ SQL;
$summary = app_query_one($pdo, $summarySql, ['tenant_id' => $tenantId]) ?? [];
$yearSummary = app_query_one(
$pdo,
<<<'SQL'
SELECT
COALESCE((
SELECT SUM(ce.strokes)
FROM coffee_entries ce
WHERE ce.tenant_id = :tenant_id
AND YEAR(ce.booked_at) = YEAR(CURRENT_DATE())
), 0) AS year_strokes,
COALESCE((
SELECT SUM(ce.total_cost)
FROM coffee_entries ce
WHERE ce.tenant_id = :tenant_id
AND YEAR(ce.booked_at) = YEAR(CURRENT_DATE())
), 0) AS year_spend,
COALESCE((
SELECT SUM(pe.amount)
FROM payment_entries pe
WHERE pe.tenant_id = :tenant_id
AND YEAR(pe.booked_at) = YEAR(CURRENT_DATE())
), 0) AS year_payments,
COALESCE((
SELECT COUNT(DISTINCT ce.member_id)
FROM coffee_entries ce
WHERE ce.tenant_id = :tenant_id
AND YEAR(ce.booked_at) = YEAR(CURRENT_DATE())
), 0) AS year_active_members
SQL,
['tenant_id' => $tenantId]
) ?? [];
$recentSql = <<<'SQL'
SELECT
le.booked_at,
@@ -2382,11 +2430,68 @@ ORDER BY le.booked_at DESC
LIMIT 10
SQL;
$memberOverviewSql = <<<'SQL'
SELECT
m.id,
m.display_name,
COALESCE(SUM(le.amount), 0) AS balance,
COALESCE((
SELECT SUM(ce.strokes)
FROM coffee_entries ce
WHERE ce.member_id = m.id
AND ce.tenant_id = m.tenant_id
AND YEAR(ce.booked_at) = YEAR(CURRENT_DATE())
), 0) AS year_strokes,
COALESCE((
SELECT SUM(pe.amount)
FROM payment_entries pe
WHERE pe.member_id = m.id
AND pe.tenant_id = m.tenant_id
AND YEAR(pe.booked_at) = YEAR(CURRENT_DATE())
), 0) AS year_payments,
MAX(le.booked_at) AS last_booking_at
FROM members m
LEFT JOIN ledger_entries le ON le.member_id = m.id AND le.tenant_id = m.tenant_id
WHERE m.tenant_id = :tenant_id
AND m.status = 'active'
GROUP BY m.id, m.display_name
ORDER BY balance ASC, m.display_name ASC
LIMIT 12
SQL;
$memberRows = app_query_all($pdo, $memberOverviewSql, ['tenant_id' => $tenantId]);
$largestDebtors = array_values(array_filter(
$memberRows,
static fn(array $row): bool => (float) ($row['balance'] ?? 0) < 0
));
usort(
$largestDebtors,
static fn(array $left, array $right): int => ((float) ($left['balance'] ?? 0) <=> (float) ($right['balance'] ?? 0))
);
$largestDebtors = array_slice($largestDebtors, 0, 3);
$largestCredits = array_values(array_filter(
$memberRows,
static fn(array $row): bool => (float) ($row['balance'] ?? 0) > 0
));
usort(
$largestCredits,
static fn(array $left, array $right): int => ((float) ($right['balance'] ?? 0) <=> (float) ($left['balance'] ?? 0))
);
$largestCredits = array_slice($largestCredits, 0, 3);
return [
'active_members' => (string) ($summary['active_members'] ?? '0'),
'coffee_volume' => (string) ($summary['coffee_volume'] ?? '0.00'),
'payment_volume' => (string) ($summary['payment_volume'] ?? '0.00'),
'open_balance' => (string) ($summary['open_balance'] ?? '0.00'),
'year_strokes' => (string) ($yearSummary['year_strokes'] ?? '0'),
'year_spend' => (string) ($yearSummary['year_spend'] ?? '0.00'),
'year_payments' => (string) ($yearSummary['year_payments'] ?? '0.00'),
'year_active_members' => (string) ($yearSummary['year_active_members'] ?? '0'),
'member_rows' => $memberRows,
'largest_debtors' => $largestDebtors,
'largest_credits' => $largestCredits,
'recent_entries' => app_query_all($pdo, $recentSql, ['tenant_id' => $tenantId]),
];
}
@@ -2834,17 +2939,21 @@ function app_delete_payment_entry(PDO $pdo, string $tenantId, string $paymentId)
function app_content_for_tenant(PDO $pdo, string $tenantId): array
{
$announcements = app_query_all(
$pdo,
'SELECT id, title, message, visible_until, is_active FROM announcements WHERE tenant_id = :tenant_id ORDER BY is_active DESC, created_at DESC LIMIT 12',
['tenant_id' => $tenantId]
);
$announcements = scripts_table_exists($pdo, 'announcements')
? app_query_all(
$pdo,
'SELECT id, title, message, visible_until, is_active FROM announcements WHERE tenant_id = :tenant_id ORDER BY is_active DESC, created_at DESC LIMIT 12',
['tenant_id' => $tenantId]
)
: [];
$faq = app_query_all(
$pdo,
'SELECT id, question, answer, sort_order, is_active FROM faq_items WHERE tenant_id = :tenant_id ORDER BY is_active DESC, sort_order ASC, created_at DESC LIMIT 12',
['tenant_id' => $tenantId]
);
$faq = scripts_table_exists($pdo, 'faq_items')
? app_query_all(
$pdo,
'SELECT id, question, answer, sort_order, is_active FROM faq_items WHERE tenant_id = :tenant_id ORDER BY is_active DESC, sort_order ASC, created_at DESC LIMIT 12',
['tenant_id' => $tenantId]
)
: [];
return [
'announcements' => $announcements,
+272 -114
View File
@@ -56,6 +56,7 @@ if ($requestedPage === null) {
'/reports' => 'reports',
'/support' => 'support',
'/surveys' => 'surveys',
'/profil', '/profile' => 'profile',
'/settings' => 'settings',
'/exports' => 'exports',
'/logout' => 'logout',
@@ -65,7 +66,7 @@ if ($requestedPage === null) {
$page = (string) $requestedPage;
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$tenantPages = ['dashboard', 'members', 'ledger', 'payments', 'content', 'imports', 'reports', 'support', 'surveys', 'settings', 'exports'];
$tenantPages = ['dashboard', 'members', 'ledger', 'payments', 'content', 'imports', 'reports', 'support', 'surveys', 'profile', 'settings', 'exports'];
if ($page === 'logout' && $requestMethod === 'POST') {
app_logout();
@@ -83,6 +84,7 @@ $members = [];
$ledger = [];
$payments = [];
$content = ['announcements' => [], 'faq' => []];
$dashboardAnnouncements = [];
$memberSummary = null;
$memberReport = null;
$ledgerScope = 'all';
@@ -149,6 +151,9 @@ if ($auth !== null && $pdo instanceof PDO) {
if (in_array($page, ['dashboard', 'ledger', 'payments'], true)) {
app_handle_tenant_action($pdo, $auth);
app_handle_bulk_finance_action($pdo, $auth);
}
if ($page === 'profile') {
app_handle_profile_action($pdo, $auth);
}
@@ -216,8 +221,21 @@ if ($auth !== null && $pdo instanceof PDO) {
$paymentMembers = app_members_for_scope($pdo, (string) $auth['tenant_id'], $paymentScope);
}
if ($page === 'content') {
if ($page === 'content' || $page === 'dashboard') {
$content = app_content_for_tenant($pdo, (string) $auth['tenant_id']);
$dashboardAnnouncements = array_slice(
array_values(
array_filter(
$content['announcements'] ?? [],
static fn(array $announcement): bool => (int) ($announcement['is_active'] ?? 0) === 1
)
),
0,
3
);
}
if ($page === 'content') {
if (isset($_GET['edit_announcement']) && $_GET['edit_announcement'] !== '') {
foreach (($content['announcements'] ?? []) as $announcement) {
@@ -309,10 +327,11 @@ $guestNavItems = [
];
$primaryNavItems = $auth === null ? $guestNavItems : $tenantNavItems;
$navGroups = $auth === null ? [] : app_tenant_navigation_groups($primaryNavItems);
$accountNavItems = $auth === null ? [] : app_tenant_account_items($auth);
$headerNavItems = $primaryNavItems;
if ($auth !== null) {
$headerNavItems = [];
$primaryKeys = ['dashboard', 'members', 'ledger', 'payments'];
$primaryKeys = ['dashboard', 'ledger', 'payments'];
foreach ($primaryNavItems as $item) {
if (in_array((string) ($item['key'] ?? ''), $primaryKeys, true)) {
$headerNavItems[] = $item;
@@ -324,8 +343,9 @@ $ledgerViewMode = in_array((string) ($_GET['mode'] ?? 'capture'), ['capture', 'j
? (string) ($_GET['mode'] ?? 'capture')
: 'capture';
$mobileBottomNavItems = [];
$mobileDrawerPrimaryItems = [];
if ($auth !== null) {
foreach (['dashboard', 'members', 'ledger'] as $mobileKey) {
foreach (['dashboard', 'ledger', 'payments'] as $mobileKey) {
foreach ($primaryNavItems as $item) {
if ((string) ($item['key'] ?? '') === $mobileKey) {
$mobileBottomNavItems[] = $item;
@@ -341,6 +361,9 @@ foreach ($primaryNavItems as $item) {
break;
}
}
if ($page === 'profile') {
$currentNavLabel = 'Persönliche Einstellungen';
}
$themeCss = app_tenant_theme_root_css($tenantSettings);
$isMarketingHome = $page === 'home' && $auth === null;
$heroEyebrow = 'Für Teams, Büros und Standorte';
@@ -409,7 +432,7 @@ $marketing = app_marketing_messages();
.site-more__group{display:grid;gap:8px;padding:8px;border-radius:12px;background:var(--surface-soft)}
.site-more__label{font-size:.76rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--text-faint)}
.site-mobile{display:none}
.site-mobile__toggle{display:inline-flex;align-items:center;gap:10px;cursor:pointer;min-height:38px;padding:.45rem .72rem;border-radius:10px;border:1px solid var(--line-strong);background:rgba(255,255,255,.04);color:var(--text-strong);font-weight:600}
.site-mobile__toggle{display:none;align-items:center;gap:10px;cursor:pointer;min-height:38px;padding:.45rem .72rem;border-radius:10px;border:1px solid var(--line-strong);background:rgba(255,255,255,.04);color:var(--text-strong);font-weight:600}
.site-mobile__toggle::before{content:"";display:block;width:18px;height:2px;border-radius:999px;background:currentColor;box-shadow:0 -6px 0 currentColor,0 6px 0 currentColor}
.mobile-drawer{position:fixed;inset:0;display:none;z-index:55}
.mobile-drawer.is-open{display:block}
@@ -538,7 +561,7 @@ $marketing = app_marketing_messages();
.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-section,.landing-callout{width:calc(100vw - 24px)}.marketing-bar{height:72px;min-height:72px;margin-bottom:16px;padding:0 12px}.marketing-nav,.marketing-actions{display:none}.marketing-mobile{display:block}.landing-hero,.landing-feature-grid,.landing-step-grid,.landing-use-grid,.landing-proof-grid{grid-template-columns:1fr}.landing-hero{gap:24px;padding-top:0}.landing-section,.landing-callout{padding:20px}.marketing-shell{padding:0 0 88px}.marketing-footer{padding:10px 12px}.marketing-footer__inner{width:100%}}
@media(max-width:1180px){body.mobile-drawer-open{overflow:hidden}.page-shell{padding-bottom:94px}.site-header{margin-bottom:14px}.site-header__inner{min-height:68px;width:100%;align-items:center;padding:10px 16px}.site-brand__subtitle{font-size:.8rem}.site-nav{display:none}.site-mobile{display:block}.site-actions{gap:8px}.content{width:min(100vw - 20px,1460px);gap:14px}.hero{padding:16px}.hero .eyebrow,.hero p{display:none}.hero h1{font-size:1.5rem;margin:0}.grid-2,.grid-3,.grid-4,form.grid{grid-template-columns:1fr}.table table{min-width:0}.desktop-ledger-shell{display:none}.mobile-ledger-shell{display:grid;gap:14px}.mobile-bottom-nav{position:fixed;left:10px;right:10px;bottom:max(10px,env(safe-area-inset-bottom));z-index:45;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:4px;padding:6px;border:1px solid var(--line);border-radius:18px;background:color-mix(in srgb,var(--surface-3) 92%, rgba(var(--brand-rgb),.12) 8%);box-shadow:var(--shadow)}}
@media(max-width:1180px){body.mobile-drawer-open{overflow:hidden}.page-shell{padding-bottom:94px}.site-header{margin-bottom:14px}.site-header__inner{min-height:68px;width:100%;align-items:center;padding:10px 16px}.site-brand__subtitle{font-size:.8rem}.site-nav{display:none}.site-mobile{display:block}.site-actions{gap:8px}.site-actions .site-more{display:none}.site-mobile__toggle{display:inline-flex}.content{width:min(100vw - 20px,1460px);gap:14px}.hero{padding:16px}.hero .eyebrow,.hero p{display:none}.hero h1{font-size:1.5rem;margin:0}.grid-2,.grid-3,.grid-4,form.grid{grid-template-columns:1fr}.table table{min-width:0}.desktop-ledger-shell{display:none}.mobile-ledger-shell{display:grid;gap:14px}.mobile-bottom-nav{position:fixed;left:10px;right:10px;bottom:max(10px,env(safe-area-inset-bottom));z-index:45;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:4px;padding:6px;border:1px solid var(--line);border-radius:18px;background:color-mix(in srgb,var(--surface-3) 92%, rgba(var(--brand-rgb),.12) 8%);box-shadow:var(--shadow)}}
@media(min-width:1181px){.site-mobile{display:none}}
</style>
</head>
@@ -737,6 +760,18 @@ $marketing = app_marketing_messages();
<?php if ($auth === null): ?>
<a class="button secondary" href="/login/">Anmelden</a>
<?php else: ?>
<details class="site-more">
<summary class="site-more__toggle">Konto</summary>
<div class="site-more__panel">
<div class="site-more__group">
<div class="site-more__label">Konto</div>
<?php foreach ($accountNavItems 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; ?>
<form method="post" action="/logout/"><button type="submit" class="button secondary" style="width:100%">Abmelden</button></form>
</div>
</div>
</details>
<button type="button" class="site-mobile__toggle" data-mobile-drawer-open>Mehr</button>
<?php endif; ?>
</div>
@@ -754,14 +789,16 @@ $marketing = app_marketing_messages();
</div>
<button type="button" class="mobile-drawer__close" aria-label="Schließen" data-mobile-drawer-close>&times;</button>
</div>
<div class="mobile-drawer__section">
<div class="mobile-drawer__label">Direkt</div>
<div class="mobile-drawer__links">
<?php foreach ($mobileBottomNavItems as $item): ?>
<a href="<?= h((string) ($item['href'] ?? '/')) ?>" class="mobile-drawer__link <?= $page === (string) ($item['key'] ?? '') ? 'active' : '' ?>"><?= h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
<?php if ($mobileDrawerPrimaryItems !== []): ?>
<div class="mobile-drawer__section">
<div class="mobile-drawer__label">Weitere Kernfunktionen</div>
<div class="mobile-drawer__links">
<?php foreach ($mobileDrawerPrimaryItems as $item): ?>
<a href="<?= h((string) ($item['href'] ?? '/')) ?>" class="mobile-drawer__link <?= $page === (string) ($item['key'] ?? '') ? 'active' : '' ?>" <?= $page === (string) ($item['key'] ?? '') ? 'aria-current="page"' : '' ?>><?= h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
<?php foreach ($navGroups as $group): ?>
<div class="mobile-drawer__section">
<div class="mobile-drawer__label"><?= h((string) ($group['label'] ?? 'Mehr')) ?></div>
@@ -773,11 +810,14 @@ $marketing = app_marketing_messages();
</div>
<?php endforeach; ?>
<div class="mobile-drawer__footer">
<div class="mobile-drawer__meta">
<strong><?= h((string) ($auth['tenant_name'] ?? 'Bereich')) ?></strong>
<span class="muted"><?= h((string) ($auth['display_name'] ?? 'Angemeldet')) ?></span>
<div class="mobile-drawer__section">
<div class="mobile-drawer__label">Konto</div>
<div class="mobile-drawer__links">
<?php foreach ($accountNavItems as $item): ?>
<a href="<?= h((string) ($item['href'] ?? '/')) ?>" class="mobile-drawer__link <?= $page === (string) ($item['key'] ?? '') ? 'active' : '' ?>" <?= $page === (string) ($item['key'] ?? '') ? 'aria-current="page"' : '' ?>><?= h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
</div>
</div>
<a href="/settings/" class="mobile-drawer__link <?= $page === 'settings' ? 'active' : '' ?>">Einstellungen</a>
<form method="post" action="/logout/"><button type="submit" class="button secondary" style="width:100%">Abmelden</button></form>
</div>
</aside>
@@ -943,9 +983,9 @@ $marketing = app_marketing_messages();
<?php endif; ?>
<?php elseif ($page === 'dashboard'): ?>
<section class="hero">
<div class="eyebrow">Übersicht</div>
<div class="eyebrow">Kaffeeliste aktuell</div>
<h1><?= h((string) $auth['tenant_name']) ?></h1>
<p>Alles Wichtige auf einen Blick.</p>
<p>Aktueller Stand der Kaffeeliste mit Salden, Jahreswerten, letzter Aktivität und den wichtigsten Arbeitswegen.</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 Verwaltung</a>
@@ -966,108 +1006,182 @@ $marketing = app_marketing_messages();
<section class="grid grid-2" style="margin-top:18px">
<article class="card">
<div class="eyebrow">Heute</div>
<h2>Schnell weiter</h2>
<div class="section-mini">
<p class="section-mini__copy">Mitglieder, Buchungen und weitere Bereiche öffnest du direkt über das Menü.</p>
<div class="eyebrow">Jahresübersicht</div>
<h2>Stand im laufenden Jahr</h2>
<div class="grid grid-3">
<article class="metric">
<strong><?= num($tenantDashboard['year_strokes'] ?? 0) ?></strong>
<h3>Striche</h3>
<p>Gebuchte Striche im aktuellen Jahr.</p>
</article>
<article class="metric">
<strong><?= money($tenantDashboard['year_spend'] ?? 0) ?></strong>
<h3>Ausgaben</h3>
<p>Gesamtkosten aus Kaffeeeinträgen.</p>
</article>
<article class="metric">
<strong><?= money($tenantDashboard['year_payments'] ?? 0) ?></strong>
<h3>Einzahlungen</h3>
<p>Alle verbuchten Einzahlungen im Jahr.</p>
</article>
</div>
</article>
<article class="card">
<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>
<h2>Kaffeeliste im Überblick</h2>
<div class="grid grid-2">
<article class="metric">
<strong><?= money($tenantDashboard['open_balance'] ?? 0) ?></strong>
<h3>Offener Gesamtsaldo</h3>
<p>Aktueller Saldo über alle Ledger-Einträge.</p>
</article>
<article class="metric">
<strong><?= num($tenantDashboard['year_active_members'] ?? 0) ?></strong>
<h3>Aktive im Jahr</h3>
<p>Mitglieder mit mindestens einem Kaffeeeintrag.</p>
</article>
</div>
<div class="actions" style="margin-top:18px">
<a class="button secondary" href="/ledger/">Buchungen öffnen</a>
<a class="button secondary" href="/payments/">Zahlungen öffnen</a>
</div>
</article>
</section>
<section class="grid grid-2" style="margin-top:18px">
<?php if ($canManageTenant): ?>
<article class="card">
<div class="eyebrow">Kernfunktion</div>
<h2>Buchungen für Kaffee erfassen</h2>
<article class="card">
<div class="eyebrow"><?= $canManageTenant ? 'Schnellaktionen' : 'Self-Service' ?></div>
<h2><?= $canManageTenant ? 'Direkt arbeiten' : 'Mein Bereich' ?></h2>
<?php if ($canManageTenant): ?>
<div class="grid grid-2">
<article class="metric">
<strong>Buchungen</strong>
<h3>Sammelerfassung öffnen</h3>
<p>Striche für alle, Vorderseite oder Rückseite direkt buchen.</p>
<div class="actions" style="margin-top:14px"><a class="button secondary" href="/ledger/">Zu den Buchungen</a></div>
</article>
<article class="metric">
<strong>Zahlungen</strong>
<h3>Einzahlungen und Korrekturen</h3>
<p>Einzahlungen buchen, stornieren und Zahlarten sauber nachführen.</p>
<div class="actions" style="margin-top:14px"><a class="button secondary" href="/payments/">Zu den Zahlungen</a></div>
</article>
</div>
<?php elseif (($tenantSettings['allow_self_service_booking'] ?? '1') === '1' && !empty($auth['member_id'])): ?>
<form method="post" action="/dashboard/" class="grid">
<input type="hidden" name="action" value="record-coffee">
<label>Mitglied<select name="member_id"><?php foreach ($members as $member): ?><option value="<?= h((string) $member['id']) ?>" <?= ((string) ($auth['member_id'] ?? '')) === (string) $member['id'] ? 'selected' : '' ?>><?= h((string) $member['display_name']) ?></option><?php endforeach; ?></select></label>
<label>Anzahl Striche<input type="number" name="strokes" min="1" step="1" value="1"></label>
<label>Preis pro Strich<input type="number" name="unit_price" min="0.01" step="0.01" value="0.50"></label>
<input type="hidden" name="member_id" value="<?= h((string) ($auth['member_id'] ?? '')) ?>">
<label>Anzahl Striche<input type="number" name="strokes" min="1" max="2" step="1" value="1"></label>
<label>Preis pro Strich<input type="number" name="unit_price" min="0.01" step="0.01" value="<?= h((string) ($tenantSettings['default_unit_price'] ?? '0.50')) ?>"></label>
<label>Buchungszeit<input type="datetime-local" name="booked_at" value="<?= date('Y-m-d\TH:i') ?>"></label>
<label>Quelle<input type="text" name="booking_source" value="self-service"></label>
<div class="actions"><button type="submit">Striche buchen</button></div>
<input type="hidden" name="booking_source" value="self-service">
<div class="actions"><button type="submit">Eigene Striche buchen</button></div>
</form>
</article>
<article class="card">
<div class="eyebrow">Kernfunktion</div>
<h2>Einzahlung erfassen</h2>
<form method="post" action="/dashboard/" class="grid">
<input type="hidden" name="action" value="record-payment">
<label>Mitglied<select name="member_id"><?php foreach ($members as $member): ?><option value="<?= h((string) $member['id']) ?>" <?= ((string) ($auth['member_id'] ?? '')) === (string) $member['id'] ? 'selected' : '' ?>><?= h((string) $member['display_name']) ?></option><?php endforeach; ?></select></label>
<label>Betrag<input type="number" name="amount" min="0.01" step="0.01" value="5.00"></label>
<label>Zahlungsart<select name="payment_method"><option value="manual">Manuell</option><option value="paypal">PayPal</option><option value="bank">Bank</option></select></label>
<label>Buchungszeit<input type="datetime-local" name="booked_at" value="<?= date('Y-m-d\TH:i') ?>"></label>
<div class="actions"><button type="submit">Einzahlung buchen</button></div>
</form>
</article>
<?php else: ?>
<article class="card">
<div class="eyebrow">Self-Service</div>
<h2>Mein Bereich</h2>
<?php if (($tenantSettings['allow_self_service_booking'] ?? '1') === '1' && !empty($auth['member_id'])): ?>
<form method="post" action="/dashboard/" class="grid">
<input type="hidden" name="action" value="record-coffee">
<input type="hidden" name="member_id" value="<?= h((string) ($auth['member_id'] ?? '')) ?>">
<label>Anzahl Striche<input type="number" name="strokes" min="1" max="2" step="1" value="1"></label>
<label>Preis pro Strich<input type="number" name="unit_price" min="0.01" step="0.01" value="<?= h((string) ($tenantSettings['default_unit_price'] ?? '0.50')) ?>"></label>
<label>Buchungszeit<input type="datetime-local" name="booked_at" value="<?= date('Y-m-d\TH:i') ?>"></label>
<input type="hidden" name="booking_source" value="self-service">
<div class="actions"><button type="submit">Eigene Striche buchen</button></div>
</form>
<?php else: ?>
<ul class="list">
<li>Hier siehst du deinen Kontostand, letzte Buchungen und aktuelle Hinweise.</li>
<li>Self-Service-Striche sind in diesem Mandanten aktuell deaktiviert.</li>
<li>Wenn dir etwas fehlt, wende dich an den Betreiber deines Bereichs.</li>
</ul>
<?php endif; ?>
</article>
<article class="card">
<div class="eyebrow">Profil</div>
<h2>Name und Passwort pflegen</h2>
<form method="post" action="/dashboard/" class="grid">
<input type="hidden" name="action" value="save-profile">
<label>Anzeigename<input name="display_name" value="<?= h((string) ($auth['display_name'] ?? '')) ?>" required></label>
<label>Neues Passwort<input type="password" name="password" placeholder="Nur bei Änderung ausfüllen"></label>
<label style="grid-column:1 / -1;">Hinweis zu Einzahlungen<textarea readonly><?= h((string) ($tenantSettings['payment_hint'] ?? '')) ?></textarea></label>
<div class="actions"><button type="submit">Profil speichern</button></div>
</form>
<?php $paypalBaseUrl = trim((string) ($tenantSettings['paypal_me_url'] ?? '')); ?>
<?php if ($paypalBaseUrl !== ''): ?>
<div class="section-mini" style="margin-top:18px">
<h3 class="section-mini__title">PayPal-Einzahlungen</h3>
<p class="section-mini__copy">Feste Beträge und der offene Saldo lassen sich direkt öffnen.</p>
<div class="actions">
<?php $balance = (float) ($memberSummary['balance'] ?? 0); ?>
<?php if ($balance < 0): ?>
<a class="button secondary" target="_blank" href="<?= h(rtrim($paypalBaseUrl, '/') . '/' . number_format(abs($balance), 2, '.', '')) ?>"><?= h(number_format(abs($balance), 2, ',', '.')) ?> EUR ausgleichen</a>
<?php endif; ?>
<?php foreach ([5, 10, 15] as $quickAmount): ?>
<a class="button secondary" target="_blank" href="<?= h(rtrim($paypalBaseUrl, '/') . '/' . number_format((float) $quickAmount, 2, '.', '')) ?>"><?= h(number_format((float) $quickAmount, 2, ',', '.')) ?> EUR</a>
<?php else: ?>
<ul class="list">
<li>Hier siehst du deinen Kontostand, letzte Buchungen und aktuelle Hinweise.</li>
<li>Self-Service-Striche sind in diesem Mandanten aktuell deaktiviert.</li>
<li>Wenn dir etwas fehlt, wende dich an den Betreiber deines Bereichs.</li>
</ul>
<?php endif; ?>
</article>
<article class="card">
<div class="eyebrow"><?= $canManageTenant ? 'Saldo-Lage' : 'Konto' ?></div>
<h2><?= $canManageTenant ? 'Auffällige Kontostände' : 'Persönliche Einstellungen' ?></h2>
<?php if ($canManageTenant): ?>
<div class="grid grid-2">
<div>
<h3 style="margin-bottom:10px">Höchste Rückstände</h3>
<ul class="list">
<?php foreach (($tenantDashboard['largest_debtors'] ?? []) as $row): ?>
<li><?= h((string) ($row['display_name'] ?? 'Mitglied')) ?> · <?= money($row['balance'] ?? 0) ?></li>
<?php endforeach; ?>
</div>
<?php if (($tenantDashboard['largest_debtors'] ?? []) === []): ?><li>Aktuell keine negativen Salden.</li><?php endif; ?>
</ul>
</div>
<?php endif; ?>
</article>
<?php endif; ?>
<div>
<h3 style="margin-bottom:10px">Größte Guthaben</h3>
<ul class="list">
<?php foreach (($tenantDashboard['largest_credits'] ?? []) as $row): ?>
<li><?= h((string) ($row['display_name'] ?? 'Mitglied')) ?> · <?= money($row['balance'] ?? 0) ?></li>
<?php endforeach; ?>
<?php if (($tenantDashboard['largest_credits'] ?? []) === []): ?><li>Aktuell keine Guthaben über 0 EUR.</li><?php endif; ?>
</ul>
</div>
</div>
<?php else: ?>
<p>Profil, Passwort und Anzeigemodus liegen in deiner persönlichen Einstellungsseite.</p>
<div class="actions" style="margin-top:18px">
<a class="button secondary" href="/profile/">Persönliche Einstellungen öffnen</a>
</div>
<p class="muted" style="margin-top:12px">Aktuell aktiv: <?= h($themeMode === 'dark' ? 'Dunkelmodus' : 'Hellmodus') ?></p>
<?php endif; ?>
</article>
</section>
<section class="card" style="margin-top:18px">
<h2>Letzte Buchungen</h2>
<div class="table">
<table>
<thead><tr><th>Zeit</th><?php if ($canManageTenant): ?><th>Mitglied</th><?php endif; ?><th>Typ</th><th>Referenz</th><th>Betrag</th></tr></thead>
<tbody><?php foreach (($canManageTenant ? $ledger : ($memberSummary['recent_entries'] ?? [])) as $entry): ?><tr><td><?= dt((string) ($entry['booked_at'] ?? '')) ?></td><?php if ($canManageTenant): ?><td><?= h((string) ($entry['member_name'] ?? '')) ?></td><?php endif; ?><td><?= h((string) ($entry['entry_type'] ?? '')) ?></td><td><?= h((string) ($entry['reference_type'] ?? '')) ?></td><td><?= money($entry['amount'] ?? 0) ?></td></tr><?php endforeach; ?></tbody>
</table>
</div>
<?php if ($canManageTenant): ?>
<section class="card" style="margin-top:18px">
<div class="actions" style="justify-content:space-between;margin-bottom:14px">
<div>
<div class="eyebrow">Kaffeelisten-Stand</div>
<h2 style="margin:0">Aktive Mitglieder im Überblick</h2>
</div>
<a class="button secondary" href="/members/">Mitgliederverwaltung öffnen</a>
</div>
<div class="table">
<table>
<thead><tr><th>Mitglied</th><th>Saldo</th><th>Striche Jahr</th><th>Einzahlungen Jahr</th><th>Letzte Aktivität</th></tr></thead>
<tbody>
<?php foreach (($tenantDashboard['member_rows'] ?? []) as $row): ?>
<tr>
<td><?= h((string) ($row['display_name'] ?? '')) ?></td>
<td><?= money($row['balance'] ?? 0) ?></td>
<td><?= num($row['year_strokes'] ?? 0) ?></td>
<td><?= money($row['year_payments'] ?? 0) ?></td>
<td><?= dt((string) ($row['last_booking_at'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</section>
<?php endif; ?>
<section class="grid grid-2" style="margin-top:18px">
<article class="card">
<div class="eyebrow">Verlauf</div>
<h2>Letzte Buchungen</h2>
<div class="table">
<table>
<thead><tr><th>Zeit</th><?php if ($canManageTenant): ?><th>Mitglied</th><?php endif; ?><th>Typ</th><th>Referenz</th><th>Betrag</th></tr></thead>
<tbody><?php foreach (($canManageTenant ? $ledger : ($memberSummary['recent_entries'] ?? [])) as $entry): ?><tr><td><?= dt((string) ($entry['booked_at'] ?? '')) ?></td><?php if ($canManageTenant): ?><td><?= h((string) ($entry['member_name'] ?? '')) ?></td><?php endif; ?><td><?= h((string) ($entry['entry_type'] ?? '')) ?></td><td><?= h((string) ($entry['reference_type'] ?? '')) ?></td><td><?= money($entry['amount'] ?? 0) ?></td></tr><?php endforeach; ?></tbody>
</table>
</div>
</article>
<article class="card">
<div class="eyebrow">Hinweise</div>
<h2>Wichtige Informationen</h2>
<?php if ($dashboardAnnouncements !== []): ?>
<ul class="list">
<?php foreach ($dashboardAnnouncements as $announcement): ?>
<li>
<strong><?= h((string) ($announcement['title'] ?? 'Hinweis')) ?></strong><br>
<?= h((string) ($announcement['message'] ?? '')) ?>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p class="muted">Aktuell sind keine aktiven Hinweise veröffentlicht.</p>
<?php endif; ?>
<div class="actions" style="margin-top:18px">
<?php if ($canManageTenant): ?>
<a class="button secondary" href="/content/">Hinweise verwalten</a>
<?php else: ?>
<a class="button secondary" href="/support/">Hilfe öffnen</a>
<?php endif; ?>
</div>
</article>
</section>
<?php elseif ($page === 'members'): ?>
<section class="hero">
@@ -1376,8 +1490,57 @@ $marketing = app_marketing_messages();
<div class="stack"><?php if (($content['faq'] ?? []) === []): ?><div class="metric"><p>Aktuell keine FAQ vorhanden.</p></div><?php endif; ?><?php foreach (($content['faq'] ?? []) as $entry): ?><div class="metric"><h3><?= h((string) $entry['question']) ?></h3><p><?= nl2br(h((string) $entry['answer'])) ?></p><?php if ($canManageTenant): ?><div class="actions" style="margin-top:12px"><a class="button secondary" href="/content/?edit_faq=<?= h((string) ($entry['id'] ?? '')) ?>">Bearbeiten</a><form method="post" action="/content/"><input type="hidden" name="action" value="archive-faq"><input type="hidden" name="faq_id" value="<?= h((string) ($entry['id'] ?? '')) ?>"><button type="submit" class="button secondary"><?= !empty($entry['is_active']) ? 'Archivieren' : 'Ausblenden' ?></button></form></div><?php endif; ?></div><?php endforeach; ?></div>
</article>
</section>
<?php elseif ($page === 'profile'): ?>
<section class="hero"><div class="eyebrow">Konto</div><h1>Persönliche Einstellungen</h1><p>Hier pflegst du deinen Anzeigenamen, dein Passwort und deinen persönlichen Anzeigemodus. Diese Einstellungen gelten nur für dich.</p></section>
<section class="grid grid-2">
<article class="card">
<h2>Profil</h2>
<form method="post" action="/profile/" class="grid">
<input type="hidden" name="action" value="save-profile">
<label>Anzeigename<input name="display_name" value="<?= h((string) ($auth['display_name'] ?? '')) ?>" required></label>
<label>Neues Passwort<input type="password" name="password" placeholder="Nur bei Änderung ausfüllen"></label>
<label style="grid-column:1 / -1;">Hinweis zu Einzahlungen<textarea readonly><?= h((string) ($tenantSettings['payment_hint'] ?? '')) ?></textarea></label>
<div class="actions"><button type="submit">Profil speichern</button></div>
</form>
</article>
<article class="card">
<h2>Anzeige</h2>
<div class="stack">
<div class="metric"><h3>Anzeigemodus</h3><p>Der Hell-/Dunkelmodus wird pro Benutzer gespeichert und ist bewusst vom Mandanten getrennt.</p></div>
<form method="post" action="/profile/" class="actions">
<input type="hidden" name="action" value="save-theme-mode">
<input type="hidden" name="theme_mode" value="<?= h($themeMode === 'dark' ? 'light' : 'dark') ?>">
<button type="submit"><?= h($themeMode === 'dark' ? 'Auf Hell umstellen' : 'Auf Dunkel umstellen') ?></button>
</form>
<p class="muted">Aktuell aktiv: <?= h($themeMode === 'dark' ? 'Dunkelmodus' : 'Hellmodus') ?></p>
</div>
<?php $paypalBaseUrl = trim((string) ($tenantSettings['paypal_me_url'] ?? '')); ?>
<?php if ($paypalBaseUrl !== ''): ?>
<div class="section-mini" style="margin-top:18px">
<h3 class="section-mini__title">PayPal-Einzahlungen</h3>
<p class="section-mini__copy">Direkte Schnelllinks für feste Beträge oder den aktuellen Ausgleich.</p>
<div class="actions">
<?php $balance = (float) ($memberSummary['balance'] ?? 0); ?>
<?php if ($balance < 0): ?>
<a class="button secondary" target="_blank" href="<?= h(rtrim($paypalBaseUrl, '/') . '/' . number_format(abs($balance), 2, '.', '')) ?>"><?= h(number_format(abs($balance), 2, ',', '.')) ?> EUR ausgleichen</a>
<?php endif; ?>
<?php foreach ([5, 10, 15] as $quickAmount): ?>
<a class="button secondary" target="_blank" href="<?= h(rtrim($paypalBaseUrl, '/') . '/' . number_format((float) $quickAmount, 2, '.', '')) ?>"><?= h(number_format((float) $quickAmount, 2, ',', '.')) ?> EUR</a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</article>
</section>
<?php if ($canManageTenant && $hasTenantSettingsFeature): ?>
<section class="card" style="margin-top:18px">
<h2>Mandant</h2>
<p class="muted">Mandantenweite Einstellungen bleiben getrennt von deinem persönlichen Konto.</p>
<div class="actions" style="margin-top:14px"><a class="button secondary" href="/settings/">Zu den Mandanten-Einstellungen</a></div>
</section>
<?php endif; ?>
<?php elseif ($page === 'settings'): ?>
<section class="hero"><div class="eyebrow">Mandanten-Einstellungen</div><h1>Einstellungen</h1><p>Diese Einstellungen gelten nur für den aktuell geöffneten Mandanten und bleiben unabhängig von anderen Bereichen.</p></section>
<section class="hero"><div class="eyebrow">Mandanten-Einstellungen</div><h1>Mandanten-Einstellungen</h1><p>Diese Einstellungen gelten nur für den aktuell geöffneten Mandanten und bleiben unabhängig von anderen Bereichen.</p></section>
<section class="grid grid-2">
<article class="card">
<h2>Einstellungen speichern</h2>
@@ -1405,15 +1568,10 @@ $marketing = app_marketing_messages();
</form>
</article>
<article class="card">
<h2>Anzeige</h2>
<h2>Abgrenzung</h2>
<div class="stack">
<div class="metric"><h3>Anzeigemodus</h3><p>Der Hell/Dunkelmodus wird pro Benutzer gespeichert und gehört bewusst in die Einstellungen statt in die Hauptnavigation.</p></div>
<form method="post" action="/settings/" class="actions">
<input type="hidden" name="action" value="save-theme-mode">
<input type="hidden" name="theme_mode" value="<?= h($themeMode === 'dark' ? 'light' : 'dark') ?>">
<button type="submit"><?= h($themeMode === 'dark' ? 'Auf Hell umstellen' : 'Auf Dunkel umstellen') ?></button>
</form>
<p class="muted">Aktuell aktiv: <?= h($themeMode === 'dark' ? 'Dunkelmodus' : 'Hellmodus') ?></p>
<div class="metric"><h3>Mandantenweite Werte</h3><p>Hier werden nur Einstellungen gepflegt, die den gesamten Mandanten betreffen.</p></div>
<div class="metric"><h3>Persönliche Einstellungen</h3><p>Profil, Passwort und Anzeigemodus liegen bewusst im eigenen Bereich unter <a href="/profile/" style="text-decoration:underline;">Persönliche Einstellungen</a>.</p></div>
</div>
</article>
</section>
@@ -1576,8 +1734,8 @@ $marketing = app_marketing_messages();
<a href="<?= h((string) ($item['href'] ?? '/')) ?>" class="mobile-bottom-nav__link <?= $page === (string) ($item['key'] ?? '') ? 'active' : '' ?>" <?= $page === (string) ($item['key'] ?? '') ? 'aria-current="page"' : '' ?>>
<span class="mobile-bottom-nav__icon"><?= match ((string) ($item['key'] ?? '')) {
'dashboard' => 'U',
'members' => 'M',
'ledger' => 'B',
'payments' => 'Z',
default => '•',
} ?></span>
<span class="mobile-bottom-nav__label"><?= h((string) ($item['label'] ?? 'Link')) ?></span>
+7
View File
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
$_GET['page'] = 'profile';
require dirname(__DIR__) . '/index.php';
+7
View File
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
$_GET['page'] = 'profile';
require dirname(__DIR__) . '/index.php';
+2 -1
View File
@@ -102,8 +102,9 @@ $tenantNavItems = app_tenant_navigation_items($auth, $tenantLicense);
$tenantNavGroups = app_tenant_navigation_groups($tenantNavItems);
$tenantHeaderNavItems = [];
$mobileBottomNavItems = [];
$mobileDrawerPrimaryItems = [];
foreach ($tenantNavItems as $item) {
if (in_array((string) ($item['key'] ?? ''), ['dashboard', 'members', 'ledger', 'payments'], true)) {
if (in_array((string) ($item['key'] ?? ''), ['dashboard', 'ledger', 'payments'], true)) {
$tenantHeaderNavItems[] = $item;
$mobileBottomNavItems[] = $item;
}
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -70,8 +70,9 @@ $tenantNavItems = app_tenant_navigation_items($auth, $tenantLicense);
$tenantNavGroups = app_tenant_navigation_groups($tenantNavItems);
$tenantHeaderNavItems = [];
$mobileBottomNavItems = [];
$mobileDrawerPrimaryItems = [];
foreach ($tenantNavItems as $item) {
if (in_array((string) ($item['key'] ?? ''), ['dashboard', 'members', 'ledger', 'payments'], true)) {
if (in_array((string) ($item['key'] ?? ''), ['dashboard', 'ledger', 'payments'], true)) {
$tenantHeaderNavItems[] = $item;
$mobileBottomNavItems[] = $item;
}
+63 -4
View File
@@ -30,6 +30,14 @@
];
$layoutPrimaryNavItems = $layoutNavItems !== [] ? $layoutNavItems : $layoutGuestItems;
$layoutHeaderNavItems = $layoutPrimaryNavItems;
if (is_array($layoutAuth)) {
$layoutHeaderNavItems = [];
foreach ($layoutPrimaryNavItems as $item) {
if (in_array((string) ($item['key'] ?? ''), ['dashboard', 'ledger', 'payments'], true)) {
$layoutHeaderNavItems[] = $item;
}
}
}
$layoutCurrentLabel = 'Start';
foreach ($layoutPrimaryNavItems as $item) {
$itemHref = rtrim((string) ($item['href'] ?? '/'), '/');
@@ -39,6 +47,12 @@
break;
}
}
if ($layoutPath === '/profile') {
$layoutCurrentLabel = 'Persönliche Einstellungen';
}
$layoutAccountItems = function_exists('app_tenant_account_items')
? app_tenant_account_items($layoutAuth)
: [];
$layoutThemeCss = function_exists('app_tenant_theme_root_css')
? app_tenant_theme_root_css($layoutThemeSettings)
: '';
@@ -47,7 +61,20 @@
} else {
$layoutMobilePrimaryNavItems = array_slice($layoutPrimaryNavItems, 0, 2);
}
$layoutMobileDrawerPrimaryItems = array_slice($layoutPrimaryNavItems, count($layoutMobilePrimaryNavItems));
$layoutMobileDrawerPrimaryItems = [];
foreach ($layoutHeaderNavItems as $item) {
$itemKey = (string) ($item['key'] ?? '');
$isAlreadyPrimary = false;
foreach ($layoutMobilePrimaryNavItems as $primaryItem) {
if ((string) ($primaryItem['key'] ?? '') === $itemKey) {
$isAlreadyPrimary = true;
break;
}
}
if (!$isAlreadyPrimary) {
$layoutMobileDrawerPrimaryItems[] = $item;
}
}
$layoutMobileHasDrawer = $layoutMobileDrawerPrimaryItems !== [] || $layoutNavGroups !== [] || is_array($layoutAuth);
$layoutMobileNavColumns = count($layoutMobilePrimaryNavItems) + ($layoutMobileHasDrawer ? 1 : 0);
if ($layoutMobileNavColumns < 3) {
@@ -1209,11 +1236,43 @@
@endphp
<a href="{{ $item['href'] ?? '/' }}" class="top-nav__link {{ $isActive ? 'is-active' : '' }}" @if ($isActive) aria-current="page" @endif>{{ $item['label'] ?? 'Link' }}</a>
@endforeach
@foreach ($layoutNavGroups as $group)
<details class="top-nav__group">
<summary class="top-nav__toggle">{{ $group['label'] ?? 'Mehr' }}</summary>
<div class="top-nav__panel">
<div class="top-nav__label">{{ $group['label'] ?? 'Mehr' }}</div>
@foreach (($group['items'] ?? []) as $item)
@php
$itemHref = rtrim((string) ($item['href'] ?? '/'), '/');
$itemHref = $itemHref === '' ? '/' : $itemHref;
$isActive = $layoutPath === $itemHref;
@endphp
<a href="{{ $item['href'] ?? '/' }}" class="top-nav__link {{ $isActive ? 'is-active' : '' }}" @if ($isActive) aria-current="page" @endif>{{ $item['label'] ?? 'Link' }}</a>
@endforeach
</div>
</details>
@endforeach
</nav>
<div class="site-actions">
@if (is_array($layoutAuth))
<button type="button" class="site-actions__toggle" data-mobile-drawer-open>Mehr</button>
<details class="top-nav__group">
<summary class="top-nav__toggle">Konto</summary>
<div class="top-nav__panel">
<div class="top-nav__label">Konto</div>
@foreach ($layoutAccountItems as $item)
@php
$itemHref = rtrim((string) ($item['href'] ?? '/'), '/');
$itemHref = $itemHref === '' ? '/' : $itemHref;
$isActive = $layoutPath === $itemHref;
@endphp
<a href="{{ $item['href'] ?? '/' }}" class="top-nav__link {{ $isActive ? 'is-active' : '' }}" @if ($isActive) aria-current="page" @endif>{{ $item['label'] ?? 'Link' }}</a>
@endforeach
<form method="post" action="/logout/">
<button type="submit" class="button button--ghost" style="width: 100%;">Abmelden</button>
</form>
</div>
</details>
@else
<a class="button button--ghost" href="/login/">Anmelden</a>
@endif
@@ -1275,7 +1334,7 @@
<div class="mobile-drawer__sections">
@if ($layoutMobileDrawerPrimaryItems !== [])
<section class="mobile-drawer__section">
<div class="mobile-drawer__label">Weitere Seiten</div>
<div class="mobile-drawer__label">Weitere Kernfunktionen</div>
<div class="mobile-drawer__links">
@foreach ($layoutMobileDrawerPrimaryItems as $item)
@php
@@ -1309,7 +1368,7 @@
<div class="mobile-drawer__label">Konto</div>
@if (is_array($layoutAuth))
<div class="mobile-drawer__links">
<div class="mobile-drawer__empty">{{ $layoutAuth['tenant_name'] ?? 'Tenant' }}</div>
<a class="mobile-drawer__link {{ $layoutPath === '/profile' ? 'is-active' : '' }}" href="/profile/" @if ($layoutPath === '/profile') aria-current="page" @endif>Persönliche Einstellungen</a>
</div>
@else
<div class="mobile-drawer__links">
@@ -11,20 +11,27 @@
<style>
:root{
<?= $themeCss ?>
--line:rgba(37,24,15,.14);
--shadow:0 18px 40px rgba(37,24,15,.08);
--line:rgba(143,158,191,.16);
--line-strong:rgba(174,189,223,.26);
--shadow:0 24px 64px rgba(1,6,18,.30);
--radius:18px;
--surface-0:#0f1720;
--surface-1:rgba(18,26,36,.84);
--surface-2:rgba(23,32,45,.92);
--surface-3:rgba(29,40,55,.96);
--text-strong:#f4f7ff;
--text-soft:rgba(219,228,248,.74);
}
*{box-sizing:border-box}
body{
margin:0;
min-height:100vh;
color:#edf2ff;
color:var(--text-strong);
font-family:"Aptos","Segoe UI",sans-serif;
background:
radial-gradient(circle at top center, rgba(88,122,255,.18), transparent 26%),
radial-gradient(circle at 18% 12%, rgba(var(--brand-rgb),.16), transparent 22%),
linear-gradient(180deg,#05070d 0%,#0b1220 48%,#0d1524 100%);
linear-gradient(180deg,var(--surface-0) 0%,color-mix(in srgb,var(--surface-0) 82%, black 18%) 100%);
}
a{color:inherit;text-decoration:none}
.button,button{
@@ -42,14 +49,14 @@
}
.button--ghost{
background:transparent;
color:#f4f7ff;
border-color:rgba(201,214,255,.16);
color:var(--text-strong);
border-color:var(--line-strong);
}
.page-shell{
width:min(1460px,calc(100vw - 32px));
margin:0 auto 40px;
width:100%;
margin:0 0 34px;
display:grid;
gap:18px;
gap:16px;
}
.site-header{
position:sticky;
@@ -68,11 +75,11 @@
min-height:72px;
padding:12px 24px;
border:0;
border-bottom:1px solid rgba(137,154,188,.18);
border-bottom:1px solid var(--line);
border-radius:0;
background:rgba(8,10,18,.86);
box-shadow:none;
backdrop-filter:blur(16px);
background:color-mix(in srgb,var(--surface-1) 88%, rgba(var(--brand-rgb),.12) 12%);
box-shadow:var(--shadow);
backdrop-filter:blur(18px);
}
.site-brand{display:flex;align-items:center;gap:12px;min-width:0}
.site-brand__mark{
@@ -86,8 +93,8 @@
font-weight:800;
box-shadow:none;
}
.site-brand__title{margin:0;font-family:"Inter Tight","Aptos","Segoe UI",sans-serif;font-size:1.08rem;letter-spacing:-.04em;color:#f4f7ff}
.site-brand__subtitle{margin:2px 0 0;color:rgba(219,228,248,.72);font-size:.9rem}
.site-brand__title{margin:0;font-family:"Inter Tight","Aptos","Segoe UI",sans-serif;font-size:1.08rem;letter-spacing:-.04em;color:var(--text-strong)}
.site-brand__subtitle{margin:2px 0 0;color:var(--text-soft);font-size:.9rem}
.site-nav,.site-actions,.actions,.context{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.site-nav__link{
display:inline-flex;
@@ -95,35 +102,35 @@
justify-content:center;
min-height:34px;
padding:0 .3rem;
border-radius:8px;
border-radius:6px;
border:1px solid transparent;
color:rgba(231,238,255,.8);
font-weight:700;
color:var(--text-soft);
font-weight:600;
font-size:.96rem;
}
.site-nav__link:hover{text-decoration:none;color:#f4f7ff;background:rgba(255,255,255,.06);border-color:rgba(201,214,255,.12)}
.site-nav__link.active{background:rgba(255,255,255,.08);color:#f4f7ff;border-color:rgba(201,214,255,.12)}
.site-nav__link:hover{text-decoration:none;color:var(--text-strong);background:rgba(var(--brand-rgb),.09);border-color:rgba(var(--brand-rgb),.18)}
.site-nav__link.active{background:rgba(var(--brand-rgb),.16);color:var(--text-strong);border-color:rgba(var(--brand-rgb),.28)}
.site-more{position:relative}
.site-more[open]{z-index:25}
.site-more__toggle{display:inline-flex;align-items:center;justify-content:center;min-height:34px;padding:0 .6rem;border-radius:8px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.03);color:rgba(231,238,255,.8);font-size:.96rem;font-weight:700;cursor:pointer;list-style:none}
.site-more__toggle{display:inline-flex;align-items:center;justify-content:center;min-height:34px;padding:0 .6rem;border-radius:8px;border:1px solid var(--line);background:rgba(255,255,255,.03);color:var(--text-soft);font-size:.96rem;font-weight:600;cursor:pointer;list-style:none}
.site-more__toggle::-webkit-details-marker{display:none}
.site-more__panel{position:absolute;right:0;top:calc(100% + 10px);min-width:240px;padding:12px;border-radius:14px;border:1px solid rgba(163,183,255,.12);background:rgba(14,20,34,.96);box-shadow:0 16px 40px rgba(2,5,12,.34);display:grid;gap:8px}
.site-more__label{font-size:.76rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:rgba(219,228,248,.52)}
.site-more__panel{position:absolute;right:0;top:calc(100% + 10px);min-width:240px;padding:12px;border-radius:14px;border:1px solid var(--line);background:var(--surface-3);box-shadow:var(--shadow);display:grid;gap:8px}
.site-more__label{font-size:.76rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--text-soft)}
.hero,.card,.alert{
border:1px solid rgba(163,183,255,.12);
border:1px solid var(--line);
border-radius:16px;
background:rgba(14,20,34,.86);
box-shadow:0 16px 40px rgba(2,5,12,.34);
background:var(--surface-2);
box-shadow:var(--shadow);
}
.hero__title,.card h2,.card h3{
font-family:"Inter Tight","Aptos","Segoe UI",sans-serif;
letter-spacing:-.04em;
color:#f4f7ff;
color:var(--text-strong);
}
.muted,p{color:rgba(219,228,246,.74)}
.muted,p{color:var(--text-soft)}
.site-mobile{display:none}
.site-mobile__toggle{
display:inline-flex;
display:none;
align-items:center;
justify-content:center;
gap:10px;
@@ -131,9 +138,9 @@
min-height:38px;
padding:.45rem .72rem;
border-radius:10px;
border:1px solid rgba(201,214,255,.14);
border:1px solid var(--line-strong);
background:rgba(255,255,255,.04);
color:#f4f7ff;
color:var(--text-strong);
font-weight:700;
}
.site-mobile__toggle::before{
@@ -156,17 +163,17 @@
width:min(100vw,420px);
max-width:100%;
padding:18px 16px calc(28px + env(safe-area-inset-bottom));
background:rgba(14,20,34,.96);
border-left:1px solid rgba(163,183,255,.12);
box-shadow:0 16px 40px rgba(2,5,12,.34);
background:var(--surface-3);
border-left:1px solid var(--line);
box-shadow:var(--shadow);
display:grid;
grid-template-rows:auto 1fr auto;
gap:18px;
overflow:auto;
}
.mobile-drawer__header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}
.mobile-drawer__title{margin:0;font-size:1.08rem;color:#f4f7ff}
.mobile-drawer__subtitle{margin:4px 0 0;color:rgba(219,228,246,.74);font-size:.9rem}
.mobile-drawer__title{margin:0;font-size:1.08rem;color:var(--text-strong)}
.mobile-drawer__subtitle{margin:4px 0 0;color:var(--text-soft);font-size:.9rem}
.mobile-drawer__close{
display:inline-flex;
align-items:center;
@@ -174,15 +181,15 @@
width:40px;
height:40px;
border-radius:999px;
border:1px solid rgba(163,183,255,.12);
border:1px solid var(--line);
background:rgba(255,255,255,.04);
color:#f4f7ff;
color:var(--text-strong);
font-size:1.15rem;
font-weight:700;
cursor:pointer;
}
.mobile-drawer__section{display:grid;gap:10px}
.mobile-drawer__label{font-size:.76rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:rgba(219,228,248,.52)}
.mobile-drawer__label{font-size:.76rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--text-soft)}
.mobile-drawer__links{display:grid;gap:8px}
.mobile-drawer__link{
display:flex;
@@ -191,13 +198,13 @@
gap:12px;
padding:.9rem 1rem;
border-radius:14px;
border:1px solid rgba(163,183,255,.12);
border:1px solid var(--line);
background:rgba(255,255,255,.04);
color:#f4f7ff;
color:var(--text-strong);
font-weight:700;
}
.mobile-drawer__link.active{background:rgba(88,122,255,.16);border-color:rgba(88,122,255,.28)}
.mobile-drawer__footer{display:grid;gap:12px;padding-top:8px;border-top:1px solid rgba(163,183,255,.12)}
.mobile-drawer__footer{display:grid;gap:12px;padding-top:8px;border-top:1px solid var(--line)}
.mobile-drawer__meta{display:grid;gap:4px}
.mobile-bottom-nav{display:none}
.mobile-bottom-nav__link,.mobile-bottom-nav__toggle{
@@ -209,14 +216,14 @@
padding:0 8px;
border:0;
background:transparent;
color:rgba(231,238,255,.8);
color:var(--text-soft);
font:inherit;
font-weight:700;
}
.mobile-bottom-nav__link.active,.mobile-bottom-nav__toggle.active{color:#f4f7ff}
.mobile-bottom-nav__link.active,.mobile-bottom-nav__toggle.active{color:var(--text-strong)}
.mobile-bottom-nav__icon{font-size:1rem;line-height:1}
.mobile-bottom-nav__label{font-size:.74rem}
.content{min-width:0;display:grid;gap:18px}
.content{width:min(1260px,calc(100% - 28px));margin:0 auto;min-width:0;display:grid;gap:18px}
.hero{
padding:24px;
display:grid;
@@ -255,7 +262,7 @@
.metric{
padding:18px;
border-radius:16px;
border:1px solid rgba(163,183,255,.12);
border:1px solid var(--line);
background:rgba(255,255,255,.04);
display:grid;
gap:8px;
@@ -288,13 +295,13 @@
th,td{padding:13px 10px;border-bottom:1px solid rgba(163,183,255,.1);text-align:left;vertical-align:top}
th{font-size:.85rem;letter-spacing:.08em;text-transform:uppercase;color:rgba(193,205,232,.7)}
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(163,183,255,.14);font:inherit;background:rgba(9,13,24,.82);color:#f4f7ff}
input,select,textarea{width:100%;padding:12px 14px;border-radius:14px;border:1px solid var(--line);font:inherit;background:rgba(9,13,24,.82);color:var(--text-strong)}
select{color-scheme:dark;background-color:rgba(9,13,24,.94)}
select option,select optgroup{background:rgba(9,13,24,.98);color:#f4f7ff}
textarea{min-height:120px}
.timeline{display:grid;gap:12px}
.timeline__item{padding:14px 16px;border-radius:16px;background:rgba(255,255,255,.04);border:1px solid rgba(163,183,255,.12)}
.timeline__meta{margin:0;color:rgba(219,228,246,.74);font-size:.94rem;line-height:1.6}
.timeline__item{padding:14px 16px;border-radius:16px;background:rgba(255,255,255,.04);border:1px solid var(--line)}
.timeline__meta{margin:0;color:var(--text-soft);font-size:.94rem;line-height:1.6}
.timeline__title{margin:0 0 6px;font-weight:800}
.pill{
display:inline-flex;
@@ -317,12 +324,13 @@
.footer{margin-top:18px;text-align:center;color:rgba(209,217,235,.68);font-size:.92rem}
@media(max-width:1180px){
body.mobile-drawer-open{overflow:hidden}
.page-shell{width:min(100vw - 20px,1460px)}
.content{width:min(100vw - 20px,1260px)}
.page-shell{padding-bottom:94px}
.site-header__inner{align-items:center;padding:12px 16px;min-height:68px;width:100%}
.site-nav{display:none}
.site-actions{gap:8px}
.site-actions .badge,.site-actions > form{display:none}
.site-actions .badge,.site-actions .site-more{display:none}
.site-mobile__toggle{display:inline-flex}
.site-mobile{display:block}
.split,.grid--2,.grid--3,.grid--4{grid-template-columns:1fr}
table{min-width:0}
@@ -336,10 +344,10 @@
grid-template-columns:repeat(4,minmax(0,1fr));
gap:4px;
padding:6px;
border:1px solid rgba(163,183,255,.12);
border:1px solid var(--line);
border-radius:18px;
background:rgba(14,20,34,.96);
box-shadow:0 16px 40px rgba(2,5,12,.34);
background:var(--surface-3);
box-shadow:var(--shadow);
}
}
@media(min-width:1181px){
@@ -378,6 +386,14 @@
<div class="site-actions">
<?= support_badge($isManager ? 'Verantwortlichen-Sicht' : 'Mitgliedersicht', 'success') ?>
<details class="site-more">
<summary class="site-more__toggle">Konto</summary>
<div class="site-more__panel">
<div class="site-more__label">Konto</div>
<a href="/profile/" class="site-nav__link">Persönliche Einstellungen</a>
<form method="post" action="/logout/"><button type="submit" class="button button--ghost" style="width:100%">Abmelden</button></form>
</div>
</details>
<button type="button" class="site-mobile__toggle" data-mobile-drawer-open>Mehr</button>
</div>
</div>
@@ -394,14 +410,16 @@
</div>
<button type="button" class="mobile-drawer__close" aria-label="Schließen" data-mobile-drawer-close>&times;</button>
</div>
<div class="mobile-drawer__section">
<div class="mobile-drawer__label">Direkt</div>
<div class="mobile-drawer__links">
<?php foreach ($mobileBottomNavItems as $item): ?>
<a href="<?= support_h((string) ($item['href'] ?? '/')) ?>" class="mobile-drawer__link <?= (($item['key'] ?? '') === 'support') ? 'active' : '' ?>" <?= (($item['key'] ?? '') === 'support') ? 'aria-current="page"' : '' ?>><?= support_h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
<?php if ($mobileDrawerPrimaryItems !== []): ?>
<div class="mobile-drawer__section">
<div class="mobile-drawer__label">Weitere Kernfunktionen</div>
<div class="mobile-drawer__links">
<?php foreach ($mobileDrawerPrimaryItems as $item): ?>
<a href="<?= support_h((string) ($item['href'] ?? '/')) ?>" class="mobile-drawer__link <?= (($item['key'] ?? '') === 'support') ? 'active' : '' ?>" <?= (($item['key'] ?? '') === 'support') ? 'aria-current="page"' : '' ?>><?= support_h((string) ($item['label'] ?? 'Link')) ?></a>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
<?php foreach ($tenantNavGroups as $group): ?>
<div class="mobile-drawer__section">
<div class="mobile-drawer__label"><?= support_h((string) ($group['label'] ?? 'Mehr')) ?></div>
@@ -413,9 +431,11 @@
</div>
<?php endforeach; ?>
<div class="mobile-drawer__footer">
<div class="mobile-drawer__meta">
<strong><?= support_h((string) ($auth['tenant_name'] ?? 'Bereich')) ?></strong>
<span class="muted"><?= support_h((string) ($auth['display_name'] ?? 'Angemeldet')) ?></span>
<div class="mobile-drawer__section">
<div class="mobile-drawer__label">Konto</div>
<div class="mobile-drawer__links">
<a href="/profile/" class="mobile-drawer__link">Persönliche Einstellungen</a>
</div>
</div>
<form method="post" action="/logout/"><button type="submit" class="button button--ghost" style="width:100%">Abmelden</button></form>
</div>
@@ -11,20 +11,27 @@
<style>
:root{
<?= $themeCss ?>
--line:rgba(37,24,15,.14);
--shadow:0 18px 40px rgba(37,24,15,.08);
--line:rgba(143,158,191,.16);
--line-strong:rgba(174,189,223,.26);
--shadow:0 24px 64px rgba(1,6,18,.30);
--radius:18px;
--surface-0:#0f1720;
--surface-1:rgba(18,26,36,.84);
--surface-2:rgba(23,32,45,.92);
--surface-3:rgba(29,40,55,.96);
--text-strong:#f4f7ff;
--text-soft:rgba(219,228,248,.74);
}
*{box-sizing:border-box}
body{
margin:0;
min-height:100vh;
font-family:"Aptos","Segoe UI",sans-serif;
color:#edf2ff;
color:var(--text-strong);
background:
radial-gradient(circle at top center, rgba(88,122,255,.18), transparent 26%),
radial-gradient(circle at 18% 12%, rgba(var(--brand-rgb),.16), transparent 22%),
linear-gradient(180deg,#05070d 0%,#0b1220 48%,#0d1524 100%);
linear-gradient(180deg,var(--surface-0) 0%,color-mix(in srgb,var(--surface-0) 82%, black 18%) 100%);
}
a{color:inherit;text-decoration:none}
h1,h2,h3{font-family:"Inter Tight","Aptos","Segoe UI",sans-serif;letter-spacing:-.04em}
@@ -43,14 +50,14 @@
}
.button--ghost{
background:transparent;
color:#f4f7ff;
border-color:rgba(201,214,255,.16);
color:var(--text-strong);
border-color:var(--line-strong);
}
.page-shell{
width:min(1460px,calc(100vw - 32px));
margin:0 auto 40px;
width:100%;
margin:0 0 34px;
display:grid;
gap:18px;
gap:16px;
}
.site-header{
position:sticky;
@@ -69,11 +76,11 @@
min-height:72px;
padding:12px 24px;
border:0;
border-bottom:1px solid rgba(137,154,188,.18);
border-bottom:1px solid var(--line);
border-radius:0;
background:rgba(8,10,18,.86);
box-shadow:none;
backdrop-filter:blur(16px);
background:color-mix(in srgb,var(--surface-1) 88%, rgba(var(--brand-rgb),.12) 12%);
box-shadow:var(--shadow);
backdrop-filter:blur(18px);
}
.site-brand{display:flex;align-items:center;gap:12px;min-width:0}
.site-brand__mark{
@@ -87,8 +94,8 @@
font-weight:800;
box-shadow:none;
}
.site-brand__title{margin:0;font-size:1.08rem;color:#f4f7ff}
.site-brand__subtitle{margin:2px 0 0;color:rgba(219,228,248,.72);font-size:.9rem}
.site-brand__title{margin:0;font-size:1.08rem;color:var(--text-strong)}
.site-brand__subtitle{margin:2px 0 0;color:var(--text-soft);font-size:.9rem}
.site-nav,.site-actions,.actions,.context,.meta{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.site-nav__link{
display:inline-flex;
@@ -96,23 +103,23 @@
justify-content:center;
min-height:34px;
padding:0 .3rem;
border-radius:8px;
border-radius:6px;
border:1px solid transparent;
color:rgba(231,238,255,.8);
font-weight:700;
color:var(--text-soft);
font-weight:600;
font-size:.96rem;
}
.site-nav__link:hover{text-decoration:none;color:#f4f7ff;background:rgba(255,255,255,.06);border-color:rgba(201,214,255,.12)}
.site-nav__link.active{background:rgba(255,255,255,.08);color:#f4f7ff;border-color:rgba(201,214,255,.12)}
.site-nav__link:hover{text-decoration:none;color:var(--text-strong);background:rgba(var(--brand-rgb),.09);border-color:rgba(var(--brand-rgb),.18)}
.site-nav__link.active{background:rgba(var(--brand-rgb),.16);color:var(--text-strong);border-color:rgba(var(--brand-rgb),.28)}
.site-more{position:relative}
.site-more[open]{z-index:25}
.site-more__toggle{display:inline-flex;align-items:center;justify-content:center;min-height:34px;padding:0 .6rem;border-radius:8px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.03);color:rgba(231,238,255,.8);font-size:.96rem;font-weight:700;cursor:pointer;list-style:none}
.site-more__toggle{display:inline-flex;align-items:center;justify-content:center;min-height:34px;padding:0 .6rem;border-radius:8px;border:1px solid var(--line);background:rgba(255,255,255,.03);color:var(--text-soft);font-size:.96rem;font-weight:600;cursor:pointer;list-style:none}
.site-more__toggle::-webkit-details-marker{display:none}
.site-more__panel{position:absolute;right:0;top:calc(100% + 10px);min-width:240px;padding:12px;border-radius:14px;border:1px solid rgba(163,183,255,.12);background:rgba(14,20,34,.96);box-shadow:0 16px 40px rgba(2,5,12,.34);display:grid;gap:8px}
.site-more__label{font-size:.76rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:rgba(219,228,248,.52)}
.site-more__panel{position:absolute;right:0;top:calc(100% + 10px);min-width:240px;padding:12px;border-radius:14px;border:1px solid var(--line);background:var(--surface-3);box-shadow:var(--shadow);display:grid;gap:8px}
.site-more__label{font-size:.76rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--text-soft)}
.site-mobile{display:none}
.site-mobile__toggle{
display:inline-flex;
display:none;
align-items:center;
justify-content:center;
gap:10px;
@@ -120,9 +127,9 @@
min-height:38px;
padding:.45rem .72rem;
border-radius:10px;
border:1px solid rgba(201,214,255,.14);
border:1px solid var(--line-strong);
background:rgba(255,255,255,.04);
color:#f4f7ff;
color:var(--text-strong);
font-weight:700;
}
.site-mobile__toggle::before{
@@ -145,17 +152,17 @@
width:min(100vw,420px);
max-width:100%;
padding:18px 16px calc(28px + env(safe-area-inset-bottom));
background:rgba(14,20,34,.96);
border-left:1px solid rgba(163,183,255,.12);
box-shadow:0 16px 40px rgba(2,5,12,.34);
background:var(--surface-3);
border-left:1px solid var(--line);
box-shadow:var(--shadow);
display:grid;
grid-template-rows:auto 1fr auto;
gap:18px;
overflow:auto;
}
.mobile-drawer__header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}
.mobile-drawer__title{margin:0;font-size:1.08rem;color:#f4f7ff}
.mobile-drawer__subtitle{margin:4px 0 0;color:rgba(219,228,246,.74);font-size:.9rem}
.mobile-drawer__title{margin:0;font-size:1.08rem;color:var(--text-strong)}
.mobile-drawer__subtitle{margin:4px 0 0;color:var(--text-soft);font-size:.9rem}
.mobile-drawer__close{
display:inline-flex;
align-items:center;
@@ -163,15 +170,15 @@
width:40px;
height:40px;
border-radius:999px;
border:1px solid rgba(163,183,255,.12);
border:1px solid var(--line);
background:rgba(255,255,255,.04);
color:#f4f7ff;
color:var(--text-strong);
font-size:1.15rem;
font-weight:700;
cursor:pointer;
}
.mobile-drawer__section{display:grid;gap:10px}
.mobile-drawer__label{font-size:.76rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:rgba(219,228,248,.52)}
.mobile-drawer__label{font-size:.76rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--text-soft)}
.mobile-drawer__links{display:grid;gap:8px}
.mobile-drawer__link{
display:flex;
@@ -180,13 +187,13 @@
gap:12px;
padding:.9rem 1rem;
border-radius:14px;
border:1px solid rgba(163,183,255,.12);
border:1px solid var(--line);
background:rgba(255,255,255,.04);
color:#f4f7ff;
color:var(--text-strong);
font-weight:700;
}
.mobile-drawer__link.active{background:rgba(88,122,255,.16);border-color:rgba(88,122,255,.28)}
.mobile-drawer__footer{display:grid;gap:12px;padding-top:8px;border-top:1px solid rgba(163,183,255,.12)}
.mobile-drawer__footer{display:grid;gap:12px;padding-top:8px;border-top:1px solid var(--line)}
.mobile-drawer__meta{display:grid;gap:4px}
.mobile-bottom-nav{display:none}
.mobile-bottom-nav__link,.mobile-bottom-nav__toggle{
@@ -198,22 +205,22 @@
padding:0 8px;
border:0;
background:transparent;
color:rgba(231,238,255,.8);
color:var(--text-soft);
font:inherit;
font-weight:700;
}
.mobile-bottom-nav__link.active,.mobile-bottom-nav__toggle.active{color:#f4f7ff}
.mobile-bottom-nav__link.active,.mobile-bottom-nav__toggle.active{color:var(--text-strong)}
.mobile-bottom-nav__icon{font-size:1rem;line-height:1}
.mobile-bottom-nav__label{font-size:.74rem}
.hero,.card,.table-card,.note{
border:1px solid rgba(163,183,255,.12);
border:1px solid var(--line);
border-radius:16px;
background:rgba(14,20,34,.86);
box-shadow:0 16px 40px rgba(2,5,12,.34);
background:var(--surface-2);
box-shadow:var(--shadow);
}
.hero__title,.card h2,.card h3{font-family:"Inter Tight","Aptos","Segoe UI",sans-serif;letter-spacing:-.04em;color:#f4f7ff}
.sidebar__subtitle,.muted{color:rgba(219,228,246,.74)}
.content{min-width:0;display:grid;gap:18px}
.hero__title,.card h2,.card h3{font-family:"Inter Tight","Aptos","Segoe UI",sans-serif;letter-spacing:-.04em;color:var(--text-strong)}
.sidebar__subtitle,.muted{color:var(--text-soft)}
.content{width:min(1260px,calc(100% - 28px));margin:0 auto;min-width:0;display:grid;gap:18px}
.hero{
display:grid;
grid-template-columns:minmax(0,1.4fr) minmax(260px,.6fr);
@@ -230,14 +237,14 @@
font-weight:800;
}
.hero__title{margin:0 0 12px;font-size:clamp(1.9rem,4vw,3rem);line-height:1.05}
.hero__lead{margin:0;color:rgba(219,228,246,.74);max-width:70ch;line-height:1.7}
.hero__lead{margin:0;color:var(--text-soft);max-width:70ch;line-height:1.7}
.hero__actions{display:flex;flex-wrap:wrap;gap:12px;margin-top:16px}
.grid{display:grid;gap:18px}
.grid--2{grid-template-columns:repeat(2,minmax(0,1fr))}
.grid--3{grid-template-columns:repeat(3,minmax(0,1fr))}
.grid--4{grid-template-columns:repeat(4,minmax(0,1fr))}
.metric{padding:18px;border:1px solid rgba(163,183,255,.12);border-radius:16px;background:rgba(255,255,255,.04)}
.metric__label{color:rgba(219,228,246,.74)}
.metric{padding:18px;border:1px solid var(--line);border-radius:16px;background:rgba(255,255,255,.04)}
.metric__label{color:var(--text-soft)}
.metric__value{font-size:1.9rem;font-weight:800;margin:6px 0 8px}
.table-card,.card{padding:22px}
.table-card__header{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;margin-bottom:16px;flex-wrap:wrap}
@@ -280,12 +287,13 @@
}
@media(max-width:1180px){
body.mobile-drawer-open{overflow:hidden}
.page-shell{width:min(100vw - 20px,1460px)}
.content{width:min(100vw - 20px,1260px)}
.page-shell{padding-bottom:94px}
.site-header__inner{align-items:center;padding:12px 16px;min-height:68px;width:100%}
.site-nav{display:none}
.site-actions{gap:8px}
.site-actions .pill,.site-actions > form{display:none}
.site-actions .pill,.site-actions .site-more{display:none}
.site-mobile__toggle{display:inline-flex}
.site-mobile{display:block}
.hero,.grid--2,.grid--3,.grid--4{grid-template-columns:1fr}
.mobile-bottom-nav{
@@ -298,10 +306,10 @@
grid-template-columns:repeat(4,minmax(0,1fr));
gap:4px;
padding:6px;
border:1px solid rgba(163,183,255,.12);
border:1px solid var(--line);
border-radius:18px;
background:rgba(14,20,34,.96);
box-shadow:0 16px 40px rgba(2,5,12,.34);
background:var(--surface-3);
box-shadow:var(--shadow);
}
}
@media(min-width:1181px){
@@ -340,6 +348,14 @@
<div class="site-actions">
<span class="pill">Rollenmatrix</span>
<details class="site-more">
<summary class="site-more__toggle">Konto</summary>
<div class="site-more__panel">
<div class="site-more__label">Konto</div>
<a class="site-nav__link" href="/profile/">Persönliche Einstellungen</a>
<form method="post" action="/logout/"><button type="submit" class="button button--ghost" style="width:100%">Abmelden</button></form>
</div>
</details>
<button type="button" class="site-mobile__toggle" data-mobile-drawer-open>Mehr</button>
</div>
</div>
@@ -356,14 +372,16 @@
</div>
<button type="button" class="mobile-drawer__close" aria-label="Schließen" data-mobile-drawer-close>&times;</button>
</div>
<div class="mobile-drawer__section">
<div class="mobile-drawer__label">Direkt</div>
<div class="mobile-drawer__links">
<?php foreach ($mobileBottomNavItems as $item): ?>
<a class="mobile-drawer__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; ?>
<?php if ($mobileDrawerPrimaryItems !== []): ?>
<div class="mobile-drawer__section">
<div class="mobile-drawer__label">Weitere Kernfunktionen</div>
<div class="mobile-drawer__links">
<?php foreach ($mobileDrawerPrimaryItems as $item): ?>
<a class="mobile-drawer__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; ?>
</div>
</div>
</div>
<?php endif; ?>
<?php foreach ($tenantNavGroups as $group): ?>
<div class="mobile-drawer__section">
<div class="mobile-drawer__label"><?= tenant_roles_h((string) ($group['label'] ?? 'Mehr')) ?></div>
@@ -375,9 +393,11 @@
</div>
<?php endforeach; ?>
<div class="mobile-drawer__footer">
<div class="mobile-drawer__meta">
<strong><?= tenant_roles_h((string) ($auth['tenant_name'] ?? 'Bereich')) ?></strong>
<span class="muted"><?= tenant_roles_h((string) ($auth['display_name'] ?? 'Angemeldet')) ?></span>
<div class="mobile-drawer__section">
<div class="mobile-drawer__label">Konto</div>
<div class="mobile-drawer__links">
<a class="mobile-drawer__link" href="/profile/">Persönliche Einstellungen</a>
</div>
</div>
<form method="post" action="/logout/"><button type="submit" class="button button--ghost" style="width:100%">Abmelden</button></form>
</div>