Anpassung des Menüs

This commit is contained in:
2026-03-30 17:39:44 +02:00
parent f96d2bc681
commit 118d0a23f7
6 changed files with 12 additions and 383 deletions
+3 -54
View File
@@ -352,57 +352,6 @@ function app_primary_role_label(?array $auth): string
return $labels[0] ?? 'Mitglied';
}
/**
* @return array<int, array{href:string,label:string}>
*/
function app_tenant_nav_items(?array $auth, array $features = []): array
{
if (!is_array($auth)) {
return [
['href' => '/', 'label' => 'Start'],
['href' => '/login/', 'label' => 'Anmeldung'],
['href' => '/admin/login/', 'label' => 'Betreiber'],
];
}
$items = [
['href' => '/dashboard/', 'label' => 'Dashboard'],
['href' => '/content/', 'label' => 'Hinweise'],
['href' => '/support/', 'label' => 'Support'],
['href' => '/surveys/', 'label' => 'Umfragen'],
];
if (app_can_manage_tenant($auth)) {
$items[] = ['href' => '/members/', 'label' => 'Mitglieder'];
}
if (app_can_manage_finance($auth)) {
$items[] = ['href' => '/ledger/', 'label' => 'Buchungen'];
$items[] = ['href' => '/payments/', 'label' => 'Einzahlungen'];
}
if (app_can_manage_tenant($auth)) {
$items[] = ['href' => '/tenants/roles/', 'label' => 'Rollen'];
}
if (app_can_manage_tenant($auth) && !empty($features['tenant_settings'])) {
$items[] = ['href' => '/settings/', 'label' => 'Einstellungen'];
}
if (
app_can_manage_tenant($auth)
&& (
!empty($features['pdf_export'])
|| !empty($features['paper_strike_entry'])
|| !empty($features['basic_exports'])
)
) {
$items[] = ['href' => '/exports/', 'label' => 'Exporte'];
}
return $items;
}
/**
* @param array<int, string> $roles
*/
@@ -442,7 +391,7 @@ function app_tenant_navigation_items(?array $auth, array $license = []): array
$features = is_array($license['features'] ?? null) ? $license['features'] : [];
$canManage = app_can_manage_tenant($auth);
$canFinance = $canManage || app_auth_has_any_role($auth, ['finance_admin']);
$canFinance = app_can_manage_finance($auth);
$hasExports = !empty($features['pdf_export']) || !empty($features['paper_strike_entry']) || !empty($features['basic_exports']);
$items = [
@@ -2675,8 +2624,8 @@ function app_handle_tenant_action(PDO $pdo, array $auth): void
return;
}
if (!app_can_manage_tenant($auth)) {
app_flash('Für diese Aktion brauchst du Tenant-Admin-Rechte.', 'warning');
if (!app_can_manage_finance($auth)) {
app_flash('Für diese Aktion brauchst du Finanz- oder Tenant-Rechte.', 'warning');
app_redirect('/dashboard/');
}
+9 -3
View File
@@ -208,10 +208,16 @@ if ($auth !== null && $pdo instanceof PDO) {
$loginState = $loginFlow['state'] ?? [];
$loginStep = (string) ($loginState['step'] ?? 'discover');
$selectedMembership = $loginState['selected_membership'] ?? null;
$restrictedPages = ['members', 'ledger', 'payments', 'settings', 'exports'];
$restrictedPages = [
'members' => static fn(array $user): bool => app_can_manage_tenant($user),
'ledger' => static fn(array $user): bool => app_can_manage_finance($user),
'payments' => static fn(array $user): bool => app_can_manage_finance($user),
'settings' => static fn(array $user): bool => app_can_manage_tenant($user),
'exports' => static fn(array $user): bool => app_can_manage_tenant($user),
];
if ($auth !== null && in_array($page, $restrictedPages, true) && !app_can_manage_tenant($auth)) {
app_flash('Dieser Bereich ist nur für Tenant-Admins verfügbar.', 'warning');
if ($auth !== null && isset($restrictedPages[$page]) && !$restrictedPages[$page]($auth)) {
app_flash('Dieser Bereich ist für deine Rolle nicht freigegeben.', 'warning');
app_redirect('/dashboard/');
}
-1
View File
@@ -7,7 +7,6 @@ if (session_status() !== PHP_SESSION_ACTIVE) {
}
require_once __DIR__ . '/../app-support.php';
require_once __DIR__ . '/../tenant-shell.php';
require_once __DIR__ . '/../../app/Modules/Support/bootstrap.php';
if (!isset($_SESSION['support_csrf'])) {
-1
View File
@@ -10,7 +10,6 @@ if (session_status() !== PHP_SESSION_ACTIVE) {
}
require_once dirname(__DIR__) . '/app-support.php';
require_once dirname(__DIR__) . '/tenant-shell.php';
require_once dirname(__DIR__, 2) . '/app/Modules/Surveys/Domain/Survey.php';
require_once dirname(__DIR__, 2) . '/app/Modules/Surveys/Domain/SurveyQuestion.php';
require_once dirname(__DIR__, 2) . '/app/Modules/Surveys/Domain/SurveyPublication.php';
-323
View File
@@ -1,323 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/app-support.php';
function tenant_shell_h(string $value): string
{
return app_h($value);
}
function tenant_shell_styles(): string
{
return <<<'HTML'
<style>
:root{
--bg:#f4efe4;
--card:#fffdf8;
--card-soft:#f7f1e5;
--ink:#25180f;
--muted:#63584a;
--brand:#005e3f;
--brand-strong:#00452f;
--accent:#c18a00;
--line:rgba(37,24,15,.14);
--shadow:0 16px 36px rgba(37,24,15,.08);
--radius:18px;
}
*{box-sizing:border-box}
body{
margin:0;
color:var(--ink);
font-family:"Aptos","Segoe UI",sans-serif;
background:linear-gradient(180deg,#f9f6ef 0%,var(--bg) 100%);
}
a{color:inherit;text-decoration:none}
.tenant-shell{width:min(1240px,calc(100vw - 32px));margin:20px auto 40px}
.tenant-header,
.tenant-nav,
.tenant-context,
.hero,
.card,
.panel,
.table-card,
.note,
.alert{
border:1px solid var(--line);
border-radius:var(--radius);
background:var(--card);
box-shadow:var(--shadow);
}
.tenant-header{
display:flex;
justify-content:space-between;
align-items:flex-start;
gap:16px;
padding:18px 22px;
margin-bottom:14px;
}
.tenant-brand{display:grid;gap:4px}
.tenant-brand__title,
.hero__title,
.card h2,
.card h3,
.panel h2,
.panel h3,
.table-card h2,
.table-card h3{
margin:0;
font-family:Georgia,serif;
letter-spacing:-.02em;
}
.tenant-brand__title{font-size:1.18rem}
.tenant-brand__subtitle,
.muted,
p{color:var(--muted)}
.tenant-toolbar,
.tenant-nav__items,
.hero__actions,
.hero__meta,
.actions,
.context,
.stack-inline{
display:flex;
flex-wrap:wrap;
gap:10px;
align-items:center;
}
.tenant-nav{
display:flex;
flex-wrap:wrap;
gap:14px;
align-items:center;
padding:14px 18px;
margin-bottom:14px;
background:var(--card-soft);
}
.tenant-nav__label{
font-size:.86rem;
font-weight:800;
letter-spacing:.12em;
text-transform:uppercase;
color:var(--accent);
}
.tenant-nav__items{gap:8px}
.tenant-nav__link,
.button,
button{
display:inline-flex;
align-items:center;
justify-content:center;
padding:10px 14px;
border-radius:999px;
border:1px solid transparent;
font:inherit;
font-weight:700;
cursor:pointer;
}
.tenant-nav__link{
background:#fff;
color:var(--brand-strong);
border-color:rgba(0,94,63,.12);
}
.tenant-nav__link.is-active,
.button,
button{
background:var(--brand);
color:#fff;
}
.button.secondary{
background:#fff;
color:var(--brand-strong);
border-color:rgba(0,94,63,.18);
}
.tenant-context{
display:flex;
flex-wrap:wrap;
gap:10px;
align-items:center;
padding:14px 18px;
margin-bottom:18px;
}
.badge,
.pill{
display:inline-flex;
align-items:center;
gap:8px;
padding:6px 11px;
border-radius:999px;
border:1px solid rgba(0,94,63,.12);
background:#fff;
color:var(--brand-strong);
font-size:.84rem;
font-weight:700;
}
.badge--solid,
.badge--success{background:rgba(0,94,63,.1);color:var(--brand-strong)}
.badge--warning{background:rgba(193,138,0,.14);color:#8c6500}
.badge--danger{background:rgba(154,31,31,.12);color:#9a1f1f}
.badge--neutral{background:rgba(0,94,63,.06);color:var(--brand-strong)}
.hero{
display:grid;
gap:18px;
padding:24px;
margin-bottom:18px;
background:linear-gradient(180deg,#fffdf8 0%,#f9f4ea 100%);
}
.hero--split{grid-template-columns:minmax(0,1.2fr) minmax(280px,.8fr)}
.hero__content,
.hero__aside,
.stack{display:grid;gap:14px}
.hero__kicker,
.card__eyebrow,
.eyebrow{
margin:0 0 8px;
font-size:.8rem;
font-weight:800;
text-transform:uppercase;
letter-spacing:.12em;
color:var(--accent);
}
.hero__title{font-size:clamp(1.9rem,4vw,3rem);line-height:1.05}
.hero__lead{margin:0;max-width:64ch;line-height:1.6}
.grid{display:grid;gap:18px}
.grid--2{grid-template-columns:repeat(2,minmax(0,1fr))}
.grid--3{grid-template-columns:repeat(3,minmax(0,1fr))}
.grid--4{grid-template-columns:repeat(4,minmax(0,1fr))}
.split{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(360px,.9fr);gap:18px}
.card,.panel,.table-card{padding:22px}
.table-card__header{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;margin-bottom:16px;flex-wrap:wrap}
.table{overflow-x:auto}
table{width:100%;border-collapse:collapse;min-width:720px}
th,td{padding:12px 10px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}
th{font-size:.82rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
input,select,textarea{
width:100%;
padding:12px 14px;
border-radius:14px;
border:1px solid rgba(37,24,15,.15);
background:#fff;
color:var(--ink);
font:inherit;
}
label{display:flex;flex-direction:column;gap:8px;font-weight:700}
textarea{min-height:120px}
.metric{
padding:18px;
border-radius:16px;
border:1px solid var(--line);
background:#fff;
display:grid;
gap:8px;
}
.metric__label{color:var(--muted)}
.metric__value{font-size:1.9rem;font-weight:800}
.status{
display:inline-flex;
align-items:center;
padding:6px 10px;
border-radius:999px;
background:rgba(0,94,63,.1);
color:var(--brand-strong);
font-weight:700;
font-size:.84rem;
}
.status--warning{background:rgba(193,138,0,.14);color:#8c6500}
.status--success{background:rgba(0,94,63,.12);color:var(--brand-strong)}
.status--danger{background:rgba(154,31,31,.12);color:#9a1f1f}
.note,
.alert{
padding:14px 16px;
}
.alert-success{background:rgba(0,94,63,.08)}
.alert-warning{background:rgba(193,138,0,.1)}
.alert-error{background:rgba(154,31,31,.1)}
.timeline{display:grid;gap:12px}
.timeline__item{
padding:14px 16px;
border-radius:16px;
border:1px solid rgba(37,24,15,.08);
background:#fff;
}
.timeline__title{margin:0 0 6px;font-weight:800;color:var(--ink)}
.timeline__meta{margin:0;color:var(--muted);line-height:1.55}
.tenant-footer{
margin-top:18px;
text-align:center;
color:var(--muted);
font-size:.92rem;
}
@media(max-width:980px){
.tenant-header,
.tenant-nav{flex-direction:column;align-items:flex-start}
.hero--split,
.grid--2,
.grid--3,
.grid--4,
.split{grid-template-columns:1fr}
table{min-width:0}
}
</style>
HTML;
}
function tenant_shell_path_is_active(string $currentPath, string $href): bool
{
$normalize = static function (string $path): string {
$value = parse_url($path, PHP_URL_PATH);
$value = is_string($value) ? rtrim($value, '/') : '';
return $value === '' ? '/' : $value;
};
return $normalize($currentPath) === $normalize($href);
}
function tenant_shell_render_header(array $auth, string $currentPath, array $tenantLicense = [], string $title = 'Kaffeeliste', string $subtitle = 'Mandantenbereich'): string
{
$navItems = app_tenant_navigation_items($auth, $tenantLicense);
$tenantName = (string) ($auth['tenant_name'] ?? 'Tenant');
$displayName = (string) ($auth['display_name'] ?? 'Benutzer');
$roleLabel = app_primary_role_label($auth);
$planName = (string) ($tenantLicense['plan_name'] ?? 'Free');
ob_start();
?>
<header class="tenant-header">
<div class="tenant-brand">
<h1 class="tenant-brand__title"><?= tenant_shell_h($title) ?></h1>
<p class="tenant-brand__subtitle"><?= tenant_shell_h($subtitle) ?></p>
</div>
<div class="tenant-toolbar">
<span class="badge"><?= tenant_shell_h($tenantName) ?></span>
<span class="badge"><?= tenant_shell_h($roleLabel) ?></span>
<span class="badge"><?= tenant_shell_h('Lizenz ' . $planName) ?></span>
</div>
</header>
<nav class="tenant-nav" aria-label="Tenant-Menue">
<span class="tenant-nav__label">Menue</span>
<div class="tenant-nav__items">
<?php foreach ($navItems as $item): ?>
<a href="<?= tenant_shell_h($item['href']) ?>" class="tenant-nav__link<?= tenant_shell_path_is_active($currentPath, (string) $item['href']) ? ' is-active' : '' ?>">
<?= tenant_shell_h((string) $item['label']) ?>
</a>
<?php endforeach; ?>
<form method="post" action="/logout/" style="display:inline-flex;">
<button type="submit" class="button secondary">Abmelden</button>
</form>
</div>
</nav>
<section class="tenant-context">
<span class="badge"><?= tenant_shell_h($displayName) ?></span>
<?php foreach (app_auth_role_labels($auth) as $label): ?>
<span class="badge badge--neutral"><?= tenant_shell_h($label) ?></span>
<?php endforeach; ?>
<?php if (!empty($auth['acting_as_platform_admin'])): ?>
<span class="badge badge--warning">Global-Admin im Tenant</span>
<?php endif; ?>
</section>
<?php
return (string) ob_get_clean();
}
-1
View File
@@ -7,7 +7,6 @@ if (session_status() !== PHP_SESSION_ACTIVE) {
}
require_once __DIR__ . '/../../app-support.php';
require_once __DIR__ . '/../../tenant-shell.php';
require_once dirname(__DIR__, 3) . '/app/Support/TenantResolver.php';
require_once dirname(__DIR__, 3) . '/app/Modules/Tenants/Models/Tenant.php';
require_once dirname(__DIR__, 3) . '/app/Modules/Tenants/Models/TenantUser.php';