Änderung Menü
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
# Mobile-Konzept 3: Buchungsseite fuer datenlastige Sammelerfassung
|
||||
|
||||
## Zielbild
|
||||
|
||||
Dieses dritte Mobile-Konzept priorisiert nicht die klassische Audit-Ansicht,
|
||||
sondern den operativen Erfassungsmodus fuer viele Buchungen in kurzer Zeit.
|
||||
Die Buchungsseite wird auf Mobilgeraeten als Arbeitsoberflaeche fuer
|
||||
Sammelerfassung, schnelle Filterung und kompaktes Nacharbeiten gedacht.
|
||||
|
||||
Ausgangspunkt:
|
||||
|
||||
- Die Legacy-Seite `stricheintragen.php` war auf hohe Eingabedichte optimiert.
|
||||
- Die aktuelle SaaS-Seite `ledger/index.blade.php` ist vor allem lesend und
|
||||
audit-orientiert.
|
||||
- Fuer mobile SaaS-Workflows braucht die Buchungsseite einen eigenen
|
||||
Erfassungsmodus statt einer nur umgebrochenen Tabelle.
|
||||
|
||||
## Priorisierung
|
||||
|
||||
### P0: Muss im ersten mobilen Konzeptionsschnitt sitzen
|
||||
|
||||
- Sammelerfassung als primaerer Einstieg
|
||||
- Filter direkt ueber der Erfassung statt in einem separaten Report-Kontext
|
||||
- Sticky Header mit wenigen, stabilen Aktionen
|
||||
- Eingabedichte ueber Listenzeilen statt ueber Karten
|
||||
- Einhand-Bedienung fuer Auswahl, Mengenanpassung und Speichern
|
||||
- Kontrolliertes Scrollverhalten ohne horizontales Tabellen-Scrolling
|
||||
|
||||
### P1: Sollte im naechsten Ausbauschritt folgen
|
||||
|
||||
- Batch-Aktionen fuer markierte Mitglieder
|
||||
- Zuletzt verwendete Filter und letzte Auswahl merken
|
||||
- Segmentierte Sichten fuer `Erfassen`, `Pruefen`, `Historie`
|
||||
- Undo-Snackbar fuer lokale Ruecknahme vor finalem Commit
|
||||
|
||||
### P2: Spaeter sinnvoll
|
||||
|
||||
- Barcode- oder QR-gestuetzte Mitgliedsauswahl
|
||||
- Offline-Zwischenspeicher fuer kurze Netzabbrueche
|
||||
- Vorlagen fuer wiederkehrende Buchungssaetze
|
||||
|
||||
## Kernprinzip
|
||||
|
||||
Mobile bekommt keine verkleinerte Ledger-Tabelle. Mobile bekommt zwei klar
|
||||
getrennte Modi auf derselben Seite:
|
||||
|
||||
1. `Erfassen`
|
||||
2. `Journal`
|
||||
|
||||
`Erfassen` ist der Default auf Smartphones. `Journal` bleibt wichtig, aber
|
||||
sekundaer und ueber ein Segment oder Bottom-Sheet erreichbar.
|
||||
|
||||
## Informationsarchitektur der Buchungsseite
|
||||
|
||||
### 1. Header
|
||||
|
||||
Der Header bleibt sticky und reduziert sich beim Scrollen auf eine kompakte
|
||||
Arbeitsleiste.
|
||||
|
||||
Inhalt im initialen Zustand:
|
||||
|
||||
- Seitentitel `Buchungen`
|
||||
- Tenant- oder Kontextlabel
|
||||
- globale Suche
|
||||
- primaere Aktion `Speichern`
|
||||
- Menue-Trigger `Mehr`
|
||||
|
||||
Inhalt im kompakten Scroll-Zustand:
|
||||
|
||||
- nur Titel oder aktiver Modus
|
||||
- Such-Trigger
|
||||
- Counter fuer offene Aenderungen, zum Beispiel `12 offen`
|
||||
- `Speichern`
|
||||
|
||||
Bewusste Entscheidung:
|
||||
|
||||
- keine KPI-Karten im mobilen Header
|
||||
- keine langen Audit-Erklaerungen oberhalb der Erfassung
|
||||
- keine zweite Navigationszeile unter dem Header
|
||||
|
||||
### 2. Modusumschalter direkt unter dem Header
|
||||
|
||||
Segmented Control mit:
|
||||
|
||||
- `Erfassen`
|
||||
- `Journal`
|
||||
|
||||
Optional spaeter:
|
||||
|
||||
- `Pruefen`
|
||||
|
||||
Der Umschalter bleibt waehrend kurzer Scrollwege sticky unter dem Header, damit
|
||||
der Wechsel zwischen Arbeits- und Kontrollsicht ohne Ruecksprung nach oben
|
||||
funktioniert.
|
||||
|
||||
### 3. Filterleiste
|
||||
|
||||
Die Filterleiste sitzt direkt unter dem Modusumschalter und ist horizontal
|
||||
scrollbar, aber aus klaren Chips aufgebaut.
|
||||
|
||||
Pflichtfilter:
|
||||
|
||||
- Bereich: `Alle`, `Vorderseite`, `Rueckseite`, `Favoriten`
|
||||
- Buchungstyp: `Striche`, `Einzahlung`, `Korrektur`
|
||||
- Status: `Nur geaendert`, `Nur offen`, `Mit Aktivitaet`
|
||||
- Suche nach Mitglied
|
||||
|
||||
Verhalten:
|
||||
|
||||
- ein aktiver Filter ist immer visuell sichtbar
|
||||
- Filter oeffnen keine Vollseiten, sondern ein Bottom-Sheet
|
||||
- zuletzt genutzte Filter bleiben fuer die Session erhalten
|
||||
|
||||
## Sammelerfassung als Hauptinteraktion
|
||||
|
||||
### Listenmodell statt Tabelle
|
||||
|
||||
Jede Mitgliederzeile wird zu einer kompakten Erfassungszeile mit fester
|
||||
Interaktionslogik:
|
||||
|
||||
- Name
|
||||
- optional Sekundaerinfo wie Team oder letzter Stand
|
||||
- Mengensteuerung fuer Striche
|
||||
- Shortcut fuer `+1`, `+2`, `+5`
|
||||
- optional numerisches Direktfeld
|
||||
|
||||
Empfohlener Zeilenaufbau:
|
||||
|
||||
- links: Identitaet
|
||||
- mitte: Schnellaktionen
|
||||
- rechts: aktueller Entwurf oder Betrag
|
||||
|
||||
Die Zeile darf auf Mobile maximal zwei Hoehen haben:
|
||||
|
||||
- Standardzustand fuer schnelles Durchscrollen
|
||||
- expandierter Zustand fuer seltene Details
|
||||
|
||||
### Eingabedichte
|
||||
|
||||
Die Seite muss 6 bis 8 bearbeitbare Zeilen gleichzeitig auf typischen
|
||||
Smartphones zeigen koennen. Das spricht gegen hohe Cards, ausufernde Labels und
|
||||
sekundaere Erklaertexte in jeder Zeile.
|
||||
|
||||
Deshalb:
|
||||
|
||||
- Zeilenhoehe kompakt halten
|
||||
- Stepper und Zahlenwert auf Daumenreichweite unten rechts ausrichten
|
||||
- Sekundaerdetails erst auf Expand oder Long-Press zeigen
|
||||
- Inline-Validierung nur bei Konflikten anzeigen
|
||||
|
||||
### Batch-Logik
|
||||
|
||||
Die Sammelerfassung braucht lokale Entwurfsdaten vor dem finalen Speichern.
|
||||
|
||||
Empfohlenes Verhalten:
|
||||
|
||||
- jede Aenderung landet sofort in einem lokalen Batch
|
||||
- der Header zeigt die Anzahl offener Aenderungen
|
||||
- `Speichern` commitet alle geaenderten Zeilen gemeinsam
|
||||
- `Verwerfen` liegt im `Mehr`-Menue
|
||||
|
||||
Das reduziert Server-Roundtrips und verhindert Unterbrechungen im
|
||||
Erfassungsfluss.
|
||||
|
||||
## Scrollverhalten
|
||||
|
||||
### Vertikales Scrollen
|
||||
|
||||
Die Seite hat genau einen primaeren Scroll-Container: die Inhaltsflaeche unter
|
||||
dem Header. Keine verschachtelten Scrollbereiche fuer Tabelle, Sidebar oder
|
||||
Filterpanel.
|
||||
|
||||
Regeln:
|
||||
|
||||
- Header sticky
|
||||
- Modusumschalter sticky
|
||||
- Filterleiste sticky, aber visuell leichter als der Header
|
||||
- Erfassungszeilen scrollen normal im Hauptflow
|
||||
|
||||
### Horizontales Scrollen
|
||||
|
||||
Horizontaler Tabellen-Scroll wird fuer Mobile bewusst vermieden. Er ist fuer
|
||||
Massenerfassung zu langsam, fehleranfaellig und verdeckt Bedienelemente.
|
||||
|
||||
Nur die Filterchips duerfen horizontal scrollen.
|
||||
|
||||
### Scroll-Feedback
|
||||
|
||||
Beim Herunterscrollen:
|
||||
|
||||
- Header komprimiert
|
||||
- sekundare Meta-Infos verschwinden
|
||||
- `Speichern` bleibt immer erreichbar
|
||||
|
||||
Beim Zurueckscrollen nach oben:
|
||||
|
||||
- voller Seitentitel kommt zurueck
|
||||
- kontextuelle Hilfe darf wieder sichtbar werden
|
||||
|
||||
## Mobile Bedienbarkeit
|
||||
|
||||
### Einhand-Nutzung
|
||||
|
||||
Die haeufigsten Aktionen muessen im unteren rechten Bereich erreichbar sein:
|
||||
|
||||
- Menge erhoehen
|
||||
- Menge reduzieren
|
||||
- Zeile markieren
|
||||
- Speichern
|
||||
|
||||
Suche und seltene Menuepunkte duerfen oben liegen. Die eigentliche
|
||||
Massenerfassung nicht.
|
||||
|
||||
### Touch-Ziele
|
||||
|
||||
- Schnellaktionen mindestens 44 px hoch
|
||||
- Zeilen interaktiv ueber die ganze Breite
|
||||
- kein kleiner Linktext fuer Kernaktionen
|
||||
|
||||
### Tastaturverhalten
|
||||
|
||||
Wenn ein numerisches Feld direkt bearbeitet wird:
|
||||
|
||||
- numerische Tastatur oeffnen
|
||||
- Fokus springt nach Speichern des Werts auf die naechste sinnvolle Zeile
|
||||
- der Header bleibt sichtbar, aber kompakter, um vertikalen Platz zu sparen
|
||||
|
||||
## Header- und Menueverhalten
|
||||
|
||||
### Header
|
||||
|
||||
Der mobile Header ist arbeitsorientiert, nicht report-orientiert.
|
||||
|
||||
Pflichtinhalte:
|
||||
|
||||
- Seitentitel oder Modus
|
||||
- Such-Trigger
|
||||
- offener Batch-Counter
|
||||
- `Speichern`
|
||||
- `Mehr`
|
||||
|
||||
Nicht in den Header:
|
||||
|
||||
- Metriken wie Saldo, Anzahl Einzahlungen, Anzahl Korrekturen
|
||||
- lange Badge-Reihen
|
||||
- Vollnavigation mit vielen Primaerlinks
|
||||
|
||||
### Menue
|
||||
|
||||
Das Menue verlagert selten genutzte oder sekundare Aktionen in ein Bottom-Sheet
|
||||
oder Slide-over.
|
||||
|
||||
Menueeintraege mobil:
|
||||
|
||||
- `Journal oeffnen`
|
||||
- `Filter zuruecksetzen`
|
||||
- `Entwurf verwerfen`
|
||||
- `Nur geaenderte zeigen`
|
||||
- `Korrekturregeln`
|
||||
- `Export` nur falls fachlich noetig
|
||||
|
||||
Die globale App-Navigation sollte mobil nicht als volle Top-Navigation laufen,
|
||||
sondern als kompaktes App-Menue oder Bottom-Navigation mit maximal 4 bis 5
|
||||
Punkten. Fuer diese Seite wichtig:
|
||||
|
||||
- `Dashboard`
|
||||
- `Mitglieder`
|
||||
- `Buchungen`
|
||||
- `Einzahlungen`
|
||||
- `Mehr`
|
||||
|
||||
`Buchungen` bleibt als fester Hauptpunkt sichtbar.
|
||||
|
||||
## Konkrete Empfehlung fuer die Buchungsseite
|
||||
|
||||
### Empfohlene Reihenfolge auf Mobile
|
||||
|
||||
1. Sticky Header mit `Buchungen`, Suche, offenem Batch-Counter, `Speichern`
|
||||
2. Segment `Erfassen | Journal`
|
||||
3. Sticky Filterchips
|
||||
4. Kompakte Erfassungsliste als Default
|
||||
5. Optionaler Sammel-Footer fuer Batch-Aktionen bei markierten Zeilen
|
||||
|
||||
### Was aus der aktuellen Seite nach unten oder raus sollte
|
||||
|
||||
- Hero-Bereich
|
||||
- KPI-Karten
|
||||
- lange Korrekturregel-Panels oberhalb der eigentlichen Buchungsliste
|
||||
- breite Ledger-Tabelle als primaere Mobile-Darstellung
|
||||
|
||||
Diese Inhalte gehoeren auf Mobile in:
|
||||
|
||||
- `Journal`
|
||||
- `Mehr`
|
||||
- separate Detail- oder Hilfesheets
|
||||
|
||||
## Warum dieses Konzept priorisiert werden sollte
|
||||
|
||||
Dieses Konzept passt am besten zu datenlastigen SaaS-Workflows, weil es nicht
|
||||
versucht, Desktop-Audit und Mobile-Erfassung in einer einzigen Darstellungsform
|
||||
zu vereinen. Es trennt operative Geschwindigkeit von auditierbarer Historie,
|
||||
haelt die Eingabedichte hoch und macht die Buchungsseite auf dem Smartphone zu
|
||||
einem echten Arbeitswerkzeug.
|
||||
|
||||
## Umsetzungsleitplanke fuer Design und Frontend
|
||||
|
||||
- Mobile first fuer `Erfassen`, nicht fuer `Journal`
|
||||
- keine horizontale Datentabelle als Kernmuster
|
||||
- lokale Batch-Verwaltung vor Persistierung
|
||||
- sticky, kompakter Header mit dauerhaftem Save-Pfad
|
||||
- Filter als Chips plus Bottom-Sheet
|
||||
- Audit- und Regeltexte nur kontextuell, nicht dauerhaft im Sichtfeld
|
||||
+1
-1
@@ -2,5 +2,5 @@
|
||||
|
||||
return <<<'SQL'
|
||||
ALTER TABLE survey_questions
|
||||
ADD COLUMN options_json LONGTEXT NULL AFTER question_type;
|
||||
MODIFY COLUMN options_json LONGTEXT NULL;
|
||||
SQL;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
return <<<'SQL'
|
||||
ALTER TABLE users
|
||||
ADD COLUMN theme_mode VARCHAR(10) NULL AFTER password_hash;
|
||||
SQL;
|
||||
@@ -409,11 +409,12 @@ function app_tenant_navigation_items(?array $auth, array $license = []): array
|
||||
|
||||
$items = [
|
||||
['key' => 'dashboard', 'label' => 'Übersicht', 'href' => '/dashboard/'],
|
||||
['key' => 'content', 'label' => 'Hinweise', 'href' => '/content/'],
|
||||
['key' => 'support', 'label' => 'Support', 'href' => '/support/'],
|
||||
['key' => 'surveys', 'label' => 'Umfragen', 'href' => '/surveys/'],
|
||||
];
|
||||
|
||||
if ($canManage) {
|
||||
$items[] = ['key' => 'members', 'label' => 'Mitglieder', 'href' => '/members/'];
|
||||
}
|
||||
|
||||
if ($canFinance) {
|
||||
$items[] = ['key' => 'ledger', 'label' => 'Buchungen', 'href' => '/ledger/'];
|
||||
$items[] = ['key' => 'payments', 'label' => 'Zahlungen', 'href' => '/payments/'];
|
||||
@@ -421,7 +422,7 @@ function app_tenant_navigation_items(?array $auth, array $license = []): array
|
||||
}
|
||||
|
||||
if ($canManage) {
|
||||
$items[] = ['key' => 'members', 'label' => 'Mitglieder', 'href' => '/members/'];
|
||||
$items[] = ['key' => 'content', 'label' => 'Hinweise & FAQ', 'href' => '/content/'];
|
||||
$items[] = ['key' => 'reports', 'label' => 'Reporting', 'href' => '/reports/'];
|
||||
$items[] = ['key' => 'roles', 'label' => 'Rollen', 'href' => '/tenants/roles/'];
|
||||
|
||||
@@ -434,14 +435,96 @@ function app_tenant_navigation_items(?array $auth, array $license = []): array
|
||||
}
|
||||
}
|
||||
|
||||
if (app_can_manage_support($auth)) {
|
||||
$items[] = ['key' => 'support', 'label' => 'Hilfe', 'href' => '/support/'];
|
||||
}
|
||||
|
||||
if (app_can_manage_surveys($auth)) {
|
||||
$items[] = ['key' => 'surveys', 'label' => 'Umfragen', 'href' => '/surveys/'];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{label:string,items:array<int, array{key:string,label:string,href:string}>}>
|
||||
*/
|
||||
function app_tenant_navigation_groups(array $items): array
|
||||
{
|
||||
$groupOrder = ['data', 'content', 'management'];
|
||||
$groupLabels = [
|
||||
'data' => 'Daten',
|
||||
'content' => 'Hilfe & Inhalte',
|
||||
'management' => 'Verwaltung',
|
||||
];
|
||||
$groupMap = [
|
||||
'imports' => 'data',
|
||||
'exports' => 'data',
|
||||
'reports' => 'data',
|
||||
'content' => 'content',
|
||||
'support' => 'content',
|
||||
'surveys' => 'content',
|
||||
'roles' => 'management',
|
||||
'settings' => 'management',
|
||||
];
|
||||
$grouped = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
$key = (string) ($item['key'] ?? '');
|
||||
$groupKey = $groupMap[$key] ?? null;
|
||||
if ($groupKey === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($grouped[$groupKey])) {
|
||||
$grouped[$groupKey] = [
|
||||
'label' => $groupLabels[$groupKey] ?? ucfirst($groupKey),
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$grouped[$groupKey]['items'][] = $item;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($groupOrder as $groupKey) {
|
||||
if (!empty($grouped[$groupKey]['items'])) {
|
||||
$result[] = $grouped[$groupKey];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
function app_users_support_theme_mode(PDO $pdo): bool
|
||||
{
|
||||
static $cache = null;
|
||||
|
||||
if (is_bool($cache)) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
$cache = app_table_has_column($pdo, 'users', 'theme_mode');
|
||||
|
||||
return $cache;
|
||||
}
|
||||
|
||||
function app_normalize_theme_mode(?string $mode): string
|
||||
{
|
||||
$mode = strtolower(trim((string) $mode));
|
||||
|
||||
return in_array($mode, ['light', 'dark'], true) ? $mode : 'dark';
|
||||
}
|
||||
|
||||
function app_platform_admin_by_email(PDO $pdo, string $email): ?array
|
||||
{
|
||||
$themeSelect = app_users_support_theme_mode($pdo)
|
||||
? ', theme_mode'
|
||||
: ", 'dark' AS theme_mode";
|
||||
|
||||
return app_query_one(
|
||||
$pdo,
|
||||
'SELECT id, email, password_hash, display_name, is_platform_admin, created_at, updated_at FROM users WHERE LOWER(email) = LOWER(:email) AND is_platform_admin = 1 LIMIT 1',
|
||||
'SELECT id, email, password_hash, display_name, is_platform_admin' . $themeSelect . ', created_at, updated_at FROM users WHERE LOWER(email) = LOWER(:email) AND is_platform_admin = 1 LIMIT 1',
|
||||
['email' => $email]
|
||||
);
|
||||
}
|
||||
@@ -679,6 +762,7 @@ function app_enter_tenant_as_platform_admin(PDO $pdo, array $admin, string $tena
|
||||
'user_id' => $admin['user_id'],
|
||||
'email' => $admin['email'],
|
||||
'display_name' => $admin['display_name'],
|
||||
'theme_mode' => app_normalize_theme_mode((string) ($admin['theme_mode'] ?? 'dark')),
|
||||
'is_platform_admin' => true,
|
||||
'tenant_id' => $tenant['id'],
|
||||
'tenant_key' => $tenant['tenant_key'],
|
||||
@@ -1197,6 +1281,37 @@ function app_handle_profile_action(PDO $pdo, array $auth): void
|
||||
app_redirect('/dashboard/');
|
||||
}
|
||||
|
||||
function app_handle_theme_action(PDO $pdo, array $auth): void
|
||||
{
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST' || (string) ($_POST['action'] ?? '') !== 'save-theme-mode') {
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = (string) ($auth['user_id'] ?? '');
|
||||
if ($userId === '') {
|
||||
app_flash('Das Theme konnte nicht gespeichert werden.', 'error');
|
||||
app_redirect(app_request_path());
|
||||
}
|
||||
|
||||
$themeMode = app_normalize_theme_mode((string) ($_POST['theme_mode'] ?? 'dark'));
|
||||
|
||||
if (app_users_support_theme_mode($pdo)) {
|
||||
app_execute(
|
||||
$pdo,
|
||||
'UPDATE users SET theme_mode = :theme_mode, updated_at = :updated_at WHERE id = :id',
|
||||
[
|
||||
'theme_mode' => $themeMode,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
'id' => $userId,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
app_set_auth_user(array_merge($auth, ['theme_mode' => $themeMode]));
|
||||
app_flash('Dein Anzeigemodus wurde gespeichert.', 'success');
|
||||
app_redirect(app_request_path());
|
||||
}
|
||||
|
||||
function app_handle_content_action(PDO $pdo, array $auth): void
|
||||
{
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
@@ -1954,11 +2069,15 @@ function app_send_print_list_pdf(PDO $pdo, string $tenantId, string $listId): vo
|
||||
|
||||
function app_memberships_by_email(PDO $pdo, string $email): array
|
||||
{
|
||||
$themeSelect = app_users_support_theme_mode($pdo)
|
||||
? 'u.theme_mode,'
|
||||
: "'dark' AS theme_mode,";
|
||||
$sql = <<<'SQL'
|
||||
SELECT
|
||||
u.id AS user_id,
|
||||
u.email AS user_email,
|
||||
u.password_hash,
|
||||
__THEME_SELECT__
|
||||
u.display_name,
|
||||
u.is_platform_admin,
|
||||
t.id AS tenant_id,
|
||||
@@ -1980,13 +2099,13 @@ LEFT JOIN roles r ON r.id = tur.role_id
|
||||
LEFT JOIN tenant_identity_providers tip ON tip.tenant_id = t.id AND tip.is_enabled = 1
|
||||
WHERE LOWER(u.email) = LOWER(:email) OR LOWER(COALESCE(m.email, '')) = LOWER(:email)
|
||||
GROUP BY
|
||||
u.id, u.email, u.password_hash, u.display_name, u.is_platform_admin,
|
||||
u.id, u.email, u.password_hash, theme_mode, u.display_name, u.is_platform_admin,
|
||||
t.id, t.tenant_key, t.name, t.status,
|
||||
tu.id, tu.status, m.id, m.email
|
||||
ORDER BY t.name ASC
|
||||
SQL;
|
||||
|
||||
return app_query_all($pdo, $sql, ['email' => $email]);
|
||||
return app_query_all($pdo, str_replace('__THEME_SELECT__', $themeSelect, $sql), ['email' => $email]);
|
||||
}
|
||||
|
||||
function app_verify_password(?string $hash, string $password): bool
|
||||
@@ -2102,6 +2221,7 @@ function app_finalize_login(array $membership): void
|
||||
'user_id' => $membership['user_id'],
|
||||
'email' => $membership['user_email'],
|
||||
'display_name' => $membership['display_name'],
|
||||
'theme_mode' => app_normalize_theme_mode((string) ($membership['theme_mode'] ?? 'dark')),
|
||||
'is_platform_admin' => (int) ($membership['is_platform_admin'] ?? 0) === 1,
|
||||
'tenant_id' => $membership['tenant_id'],
|
||||
'tenant_key' => $membership['tenant_key'],
|
||||
|
||||
+435
-60
@@ -140,6 +140,7 @@ if ($auth !== null && $pdo instanceof PDO) {
|
||||
try {
|
||||
$tenantLicense = app_tenant_license($pdo, (string) $auth['tenant_id']);
|
||||
$tenantSettings = app_tenant_settings($pdo, (string) $auth['tenant_id']);
|
||||
app_handle_theme_action($pdo, $auth);
|
||||
$hasTenantSettingsFeature = !empty($tenantLicense['features']['tenant_settings']);
|
||||
$hasPdfExportFeature = !empty($tenantLicense['features']['pdf_export']);
|
||||
$hasPaperStrikeEntryFeature = !empty($tenantLicense['features']['paper_strike_entry']);
|
||||
@@ -307,6 +308,32 @@ $guestNavItems = [
|
||||
['key' => 'tenants', 'href' => '/admin/login/', 'label' => 'Admin'],
|
||||
];
|
||||
$primaryNavItems = $auth === null ? $guestNavItems : $tenantNavItems;
|
||||
$navGroups = $auth === null ? [] : app_tenant_navigation_groups($primaryNavItems);
|
||||
$headerNavItems = $primaryNavItems;
|
||||
if ($auth !== null) {
|
||||
$headerNavItems = [];
|
||||
$primaryKeys = ['dashboard', 'members', 'ledger', 'payments'];
|
||||
foreach ($primaryNavItems as $item) {
|
||||
if (in_array((string) ($item['key'] ?? ''), $primaryKeys, true)) {
|
||||
$headerNavItems[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
$themeMode = app_normalize_theme_mode((string) ($auth['theme_mode'] ?? 'dark'));
|
||||
$ledgerViewMode = in_array((string) ($_GET['mode'] ?? 'capture'), ['capture', 'journal'], true)
|
||||
? (string) ($_GET['mode'] ?? 'capture')
|
||||
: 'capture';
|
||||
$mobileBottomNavItems = [];
|
||||
if ($auth !== null) {
|
||||
foreach (['dashboard', 'members', 'ledger'] as $mobileKey) {
|
||||
foreach ($primaryNavItems as $item) {
|
||||
if ((string) ($item['key'] ?? '') === $mobileKey) {
|
||||
$mobileBottomNavItems[] = $item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$currentNavLabel = 'Start';
|
||||
foreach ($primaryNavItems as $item) {
|
||||
if ($page === (string) ($item['key'] ?? '')) {
|
||||
@@ -354,33 +381,79 @@ $marketing = app_marketing_messages();
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Die Kaffeeliste</title>
|
||||
<style>
|
||||
:root{<?= $themeCss ?>--line:rgba(143,158,191,.16);--line-strong:rgba(174,189,223,.26);--shadow:0 24px 64px rgba(1,6,18,.36);--shadow-soft:0 16px 34px rgba(1,6,18,.2);--radius:14px;--surface-0:#05070d;--surface-1:rgba(8,12,22,.84);--surface-2:rgba(13,18,31,.92);--surface-3:rgba(17,24,40,.96);--surface-soft:rgba(255,255,255,.04);--text-strong:#f4f7ff;--text-soft:rgba(219,228,248,.74);--text-faint:rgba(190,202,226,.52)}
|
||||
:root{<?= $themeCss ?>--radius:14px;--brand-glow:rgba(var(--brand-rgb),.22);--accent-glow:rgba(var(--accent-rgb),.16)}
|
||||
:root,[data-theme="dark"]{--line:rgba(143,158,191,.16);--line-strong:rgba(174,189,223,.26);--shadow:0 24px 64px rgba(1,6,18,.30);--shadow-soft:0 16px 34px rgba(1,6,18,.16);--surface-0:#0f1720;--surface-1:rgba(18,26,36,.84);--surface-2:rgba(23,32,45,.92);--surface-3:rgba(29,40,55,.96);--surface-soft:rgba(255,255,255,.05);--text-strong:#f4f7ff;--text-soft:rgba(219,228,248,.74);--text-faint:rgba(190,202,226,.56)}
|
||||
[data-theme="light"]{--line:rgba(61,84,126,.14);--line-strong:rgba(61,84,126,.2);--shadow:0 20px 48px rgba(20,36,68,.12);--shadow-soft:0 12px 24px rgba(20,36,68,.08);--surface-0:#eef2f6;--surface-1:rgba(255,253,248,.88);--surface-2:rgba(255,255,255,.94);--surface-3:#ffffff;--surface-soft:rgba(var(--brand-rgb),.08);--text-strong:#17212f;--text-soft:rgba(37,49,72,.76);--text-faint:rgba(58,72,98,.58)}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;min-height:100vh;font-family:"Aptos","Segoe UI",sans-serif;color:var(--text-strong);background:radial-gradient(circle at top center,rgba(88,122,255,.2),transparent 26%),radial-gradient(circle at 18% 18%,rgba(var(--brand-rgb),.16),transparent 22%),linear-gradient(180deg,#05070d 0%,#0a0f1a 48%,#0d1320 100%)}
|
||||
body{margin:0;min-height:100vh;font-family:"Aptos","Segoe UI",sans-serif;color:var(--text-strong);background:radial-gradient(circle at top center,var(--brand-glow),transparent 26%),radial-gradient(circle at 18% 18%,rgba(var(--brand-rgb),.16),transparent 22%),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}
|
||||
.button,button{display:inline-flex;align-items:center;justify-content:center;padding:.72rem 1rem;border-radius:10px;border:1px solid transparent;background:linear-gradient(135deg,#587aff 0%,#243a91 100%);color:#fff;font:inherit;font-weight:700;cursor:pointer;box-shadow:0 12px 24px rgba(36,58,145,.22)}
|
||||
.button,button{display:inline-flex;align-items:center;justify-content:center;padding:.72rem 1rem;border-radius:10px;border:1px solid transparent;background:linear-gradient(135deg,var(--brand) 0%,var(--brand-strong) 100%);color:#fff;font:inherit;font-weight:700;cursor:pointer;box-shadow:0 12px 24px rgba(var(--brand-rgb),.22)}
|
||||
.button.secondary,.button--ghost{background:rgba(255,255,255,.03);color:var(--text-strong);border-color:rgba(255,255,255,.12)}
|
||||
.page-shell{width:100%;margin:0 0 34px;display:grid;gap:16px}
|
||||
.site-header{position:sticky;top:0;z-index:30;width:100vw;margin-left:calc(50% - 50vw);margin-bottom:24px}
|
||||
.site-header__inner{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:74px;width:100%;margin:0;padding:0 24px;border:0;border-bottom:1px solid var(--line);border-radius:0;background:rgba(8,10,18,.82);box-shadow:var(--shadow);backdrop-filter:blur(18px)}
|
||||
.site-header__inner{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:74px;width:100%;margin:0;padding:0 24px;border:0;border-bottom:1px solid var(--line);border-radius:0;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{width:38px;height:38px;border-radius:10px;display:grid;place-items:center;background:linear-gradient(135deg,#587aff 0%,#243a91 100%);color:#fff;font-weight:800;box-shadow:none}
|
||||
.site-brand__mark{width:38px;height:38px;border-radius:10px;display:grid;place-items:center;background:linear-gradient(135deg,var(--brand) 0%,var(--brand-strong) 100%);color:#fff;font-weight:800;box-shadow:none}
|
||||
.site-brand__title{margin:0;font-size:1rem;color:var(--text-strong);letter-spacing:-.03em}
|
||||
.site-brand__subtitle{margin:2px 0 0;color:var(--text-soft);font-size:.88rem}
|
||||
.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;min-height:34px;padding:0 .34rem;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--text-soft);font-size:.96rem;font-weight:600}
|
||||
.site-nav__link:hover{text-decoration:none;color:var(--text-strong);background:rgba(255,255,255,.05);border-color:rgba(255,255,255,.08)}
|
||||
.site-nav__link.active{background:rgba(255,255,255,.08);color:var(--text-strong);border-color:rgba(255,255,255,.12)}
|
||||
.site-mobile{display:none;position:relative}
|
||||
.site-mobile[open]{z-index:20}
|
||||
.site-toggle{display:inline-flex;align-items:center;gap:10px;cursor:pointer;list-style:none;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-toggle::-webkit-details-marker{display:none}
|
||||
.site-toggle::before{content:"";display:block;width:18px;height:2px;border-radius:999px;background:currentColor;box-shadow:0 -6px 0 currentColor,0 6px 0 currentColor}
|
||||
.site-panel{position:absolute;right:0;top:calc(100% + 12px);width:min(320px,calc(100vw - 32px));padding:14px;border-radius:16px;border:1px solid var(--line);background:var(--surface-3);box-shadow:var(--shadow)}
|
||||
.site-stack{display:grid;gap:10px}
|
||||
.site-stack .site-nav__link{justify-content:flex-start;background:rgba(255,255,255,.04);border-color:rgba(255,255,255,.08);color:var(--text-strong)}
|
||||
.site-footer-actions{display:flex;justify-content:flex-end;margin-top:12px}
|
||||
.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: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 var(--line);background:var(--surface-3);box-shadow:var(--shadow);display:grid;gap:8px}
|
||||
.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::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}
|
||||
.mobile-drawer__scrim{position:absolute;inset:0;background:rgba(4,10,18,.58);backdrop-filter:blur(6px)}
|
||||
.mobile-drawer__panel{position:absolute;right:0;top:0;bottom:0;width:min(92vw,380px);padding:18px 16px calc(28px + env(safe-area-inset-bottom));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}
|
||||
.mobile-drawer__subtitle{margin:4px 0 0;color:var(--text-soft);font-size:.9rem}
|
||||
.mobile-drawer__close{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:999px;border:1px solid var(--line);background:rgba(255,255,255,.04);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:var(--text-faint)}
|
||||
.mobile-drawer__links{display:grid;gap:8px}
|
||||
.mobile-drawer__link{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:.9rem 1rem;border-radius:14px;border:1px solid var(--line);background:rgba(255,255,255,.04);color:var(--text-strong);font-weight:700}
|
||||
.mobile-drawer__link.active{background:rgba(var(--brand-rgb),.16);border-color:rgba(var(--brand-rgb),.28)}
|
||||
.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{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:56px;padding:0 8px;border:0;background:transparent;color:var(--text-soft);font:inherit;font-weight:700}
|
||||
.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}
|
||||
.desktop-ledger-shell{display:grid;gap:18px}
|
||||
.mobile-ledger-shell{display:none}
|
||||
.mobile-screen-head{display:grid;gap:12px}
|
||||
.mobile-screen-head__top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}
|
||||
.mobile-screen-head__title{margin:0;font-size:1.55rem;line-height:1.02}
|
||||
.mobile-screen-head__copy{margin:0;color:var(--text-soft);font-size:.96rem}
|
||||
.mobile-chip-row{display:flex;gap:8px;overflow:auto;padding-bottom:2px}
|
||||
.mobile-chip-row .button{white-space:nowrap;min-height:38px;padding:.55rem .8rem}
|
||||
.mobile-segmented{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
|
||||
.mobile-segmented .button{width:100%}
|
||||
.mobile-capture-card,.mobile-journal-card{display:grid;gap:14px}
|
||||
.mobile-form-grid{display:grid;gap:12px}
|
||||
.mobile-entry-list{display:grid;gap:10px}
|
||||
.mobile-entry-row{display:grid;grid-template-columns:minmax(0,1fr) 82px;gap:12px;align-items:center;padding:14px;border:1px solid var(--line);border-radius:14px;background:rgba(255,255,255,.04)}
|
||||
.mobile-entry-row strong{display:block;font-size:1rem}
|
||||
.mobile-entry-row span{display:block;color:var(--text-soft);font-size:.84rem;line-height:1.45}
|
||||
.mobile-entry-row input{padding:11px 10px;text-align:center}
|
||||
.mobile-sticky-submit{position:sticky;bottom:84px;z-index:8;display:flex;gap:10px;align-items:center;justify-content:space-between;padding:12px 14px;border:1px solid var(--line);border-radius:16px;background:color-mix(in srgb,var(--surface-3) 90%, rgba(var(--brand-rgb),.12) 10%);box-shadow:var(--shadow-soft)}
|
||||
.mobile-sticky-submit .button{flex:0 0 auto}
|
||||
.mobile-journal-list{display:grid;gap:12px}
|
||||
.mobile-journal-item{display:grid;gap:8px;padding:14px;border:1px solid var(--line);border-radius:14px;background:rgba(255,255,255,.04)}
|
||||
.mobile-journal-item__head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}
|
||||
.mobile-journal-item__meta{display:grid;gap:4px;color:var(--text-soft);font-size:.86rem}
|
||||
.hero,.card,.alert{border:1px solid var(--line);border-radius:var(--radius);background:var(--surface-2);box-shadow:var(--shadow)}
|
||||
.sidebar__eyebrow,.eyebrow{display:inline-block;color:#8aa0d0;text-transform:uppercase;letter-spacing:.14em;font-size:.8rem;font-weight:800}
|
||||
.sidebar__eyebrow{margin:0}
|
||||
@@ -388,15 +461,19 @@ $marketing = app_marketing_messages();
|
||||
.sidebar__subtitle,.muted,p{color:var(--text-soft)}
|
||||
.content{width:min(1320px,calc(100vw - 32px));margin:0 auto;min-width:0;display:grid;gap:16px}
|
||||
.hero,.card{padding:18px}
|
||||
.hero{margin-bottom:0;background:linear-gradient(135deg,rgba(14,20,34,.98),rgba(10,15,26,.96))}
|
||||
.hero{margin-bottom:0;background:linear-gradient(135deg,rgba(var(--brand-rgb),.20),var(--surface-2) 26%,var(--surface-1) 100%)}
|
||||
.grid{display:grid;gap:18px}.grid-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-4{grid-template-columns:repeat(4,minmax(0,1fr))}
|
||||
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:14px;background:rgba(255,255,255,.04)}.metric strong{display:block;font-size:1.55rem;margin-bottom:6px}
|
||||
.metric{padding:18px;border:1px solid var(--line);border-radius:14px;background:color-mix(in srgb,var(--surface-3) 88%, rgba(var(--brand-rgb),.12) 12%)}.metric strong{display:block;font-size:1.55rem;margin-bottom:6px}
|
||||
.list{margin:14px 0 0;padding-left:18px;color:var(--text-soft);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(88,122,255,.12);color:#d9e2ff}.badge-success{background:rgba(88,122,255,.12);color:#d9e2ff}.badge-warning{background:rgba(224,154,70,.14);color:#ffd7a1}
|
||||
.badge-neutral{background:rgba(var(--brand-rgb),.12);color:var(--text-strong)}.badge-success{background:rgba(var(--brand-rgb),.12);color:var(--text-strong)}.badge-warning{background:rgba(var(--accent-rgb),.16);color:var(--text-strong)}
|
||||
.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(--text-soft);background:rgba(255,255,255,.03)}
|
||||
.bulk-shell{display:grid;gap:18px}
|
||||
.bulk-toolbar,.bulk-footer{display:flex;flex-wrap:wrap;gap:12px;align-items:center;justify-content:space-between}
|
||||
.bulk-summary{display:flex;flex-wrap:wrap;gap:10px;align-items:center;color:var(--text-soft);font-weight:600}
|
||||
.bulk-chip{display:inline-flex;align-items:center;padding:6px 10px;border-radius:999px;background:rgba(var(--brand-rgb),.12);color:var(--text-strong);font-size:.88rem;font-weight:700}
|
||||
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:12px;border:1px solid rgba(255,255,255,.08);font:inherit;background:rgba(255,255,255,.04);color:var(--text-strong)}textarea{min-height:120px}
|
||||
.footer{margin-top:0;text-align:center;color:var(--text-faint);font-size:.92rem}
|
||||
.section-mini{display:grid;gap:10px}
|
||||
@@ -461,11 +538,11 @@ $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:960px){.site-header{margin-bottom:20px}.site-header__inner{min-height:68px;width:100%;align-items:center;padding:0 16px}.site-nav{display:none}.site-mobile{display:block}.content{width:min(100vw - 20px,1460px)}.grid-2,.grid-3,.grid-4,form.grid{grid-template-columns:1fr}.table table{min-width:0}}
|
||||
@media(max-width:960px){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 .badge, .site-actions > form{display:none}.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:961px){.site-mobile{display:none}}
|
||||
</style>
|
||||
</head>
|
||||
<body class="<?= $isMarketingHome ? 'landing-preview' : '' ?>">
|
||||
<body class="<?= $isMarketingHome ? 'landing-preview' : '' ?>" data-theme="<?= h($themeMode) ?>">
|
||||
<?php if ($isMarketingHome): ?>
|
||||
<main class="marketing-shell">
|
||||
<header class="marketing-bar">
|
||||
@@ -638,9 +715,22 @@ $marketing = app_marketing_messages();
|
||||
</a>
|
||||
|
||||
<nav class="site-nav" aria-label="Hauptnavigation">
|
||||
<?php foreach ($primaryNavItems as $item): ?>
|
||||
<?php foreach ($headerNavItems 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; ?>
|
||||
<?php foreach ($navGroups as $group): ?>
|
||||
<details class="site-more">
|
||||
<summary class="site-more__toggle"><?= h((string) ($group['label'] ?? 'Mehr')) ?></summary>
|
||||
<div class="site-more__panel">
|
||||
<div class="site-more__group">
|
||||
<div class="site-more__label"><?= h((string) ($group['label'] ?? 'Mehr')) ?></div>
|
||||
<?php foreach (($group['items'] ?? []) 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; ?>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
|
||||
<div class="site-actions">
|
||||
@@ -651,25 +741,54 @@ $marketing = app_marketing_messages();
|
||||
<a class="button secondary" href="/login/">Anmelden</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<details class="site-mobile">
|
||||
<summary class="site-toggle">Menü</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>
|
||||
<?php if ($auth !== null): ?>
|
||||
<button type="button" class="site-mobile__toggle" data-mobile-drawer-open>Mehr</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<?php if ($auth !== null): ?>
|
||||
<div class="mobile-drawer" data-mobile-drawer>
|
||||
<button type="button" class="mobile-drawer__scrim" aria-label="Menü schließen" data-mobile-drawer-close></button>
|
||||
<aside class="mobile-drawer__panel" aria-label="Mobile Navigation">
|
||||
<div class="mobile-drawer__header">
|
||||
<div>
|
||||
<h2 class="mobile-drawer__title">Mehr</h2>
|
||||
<p class="mobile-drawer__subtitle"><?= h((string) ($auth['tenant_name'] ?? 'Bereich')) ?></p>
|
||||
</div>
|
||||
<button type="button" class="mobile-drawer__close" aria-label="Schließen" data-mobile-drawer-close>×</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; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php foreach ($navGroups as $group): ?>
|
||||
<div class="mobile-drawer__section">
|
||||
<div class="mobile-drawer__label"><?= h((string) ($group['label'] ?? 'Mehr')) ?></div>
|
||||
<div class="mobile-drawer__links">
|
||||
<?php foreach (($group['items'] ?? []) 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; ?>
|
||||
</div>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="content">
|
||||
<?php if ($auth !== null && !empty($auth['acting_as_platform_admin'])): ?>
|
||||
<section class="alert alert-info">
|
||||
@@ -1041,10 +1160,129 @@ $marketing = app_marketing_messages();
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($page === 'ledger'): ?>
|
||||
<section class="hero"><div class="eyebrow">Buchungen</div><h1>Striche und Buchungen</h1><p>Kaffeeeinträge und alle zugehörigen Buchungen bleiben tenantweise nachvollziehbar.</p></section>
|
||||
<section class="grid grid-2">
|
||||
<article class="card">
|
||||
<h2>Neuen Strich eintragen</h2>
|
||||
<div class="mobile-ledger-shell">
|
||||
<section class="mobile-screen-head card">
|
||||
<div class="mobile-screen-head__top">
|
||||
<div>
|
||||
<div class="eyebrow">Buchungen</div>
|
||||
<h1 class="mobile-screen-head__title">Buchungen</h1>
|
||||
</div>
|
||||
<span class="bulk-chip"><?= num(count($ledger)) ?> Journal</span>
|
||||
</div>
|
||||
<p class="mobile-screen-head__copy">Mobil steht die Erfassung im Vordergrund. Das Journal bleibt als zweite Sicht direkt erreichbar.</p>
|
||||
<div class="mobile-segmented">
|
||||
<a class="button <?= $ledgerViewMode === 'capture' ? '' : 'secondary' ?>" href="/ledger/?scope=<?= h($ledgerScope) ?>&mode=capture">Erfassen</a>
|
||||
<a class="button <?= $ledgerViewMode === 'journal' ? '' : 'secondary' ?>" href="/ledger/?scope=<?= h($ledgerScope) ?>&mode=journal">Journal</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php if ($ledgerViewMode === 'capture'): ?>
|
||||
<section class="card mobile-capture-card">
|
||||
<div class="bulk-summary">
|
||||
<span class="bulk-chip"><?= num(count($ledgerMembers)) ?> Teilnehmer</span>
|
||||
<span><?= h(['all' => 'Alle', 'front' => 'Vorderseite', 'back' => 'Rückseite'][$ledgerScope] ?? 'Alle') ?></span>
|
||||
</div>
|
||||
<div class="mobile-chip-row">
|
||||
<?php foreach (['all' => 'Alle', 'front' => 'Vorderseite', 'back' => 'Rückseite'] as $scopeKey => $scopeLabel): ?>
|
||||
<a class="button <?= $ledgerScope === $scopeKey ? '' : 'secondary' ?>" href="/ledger/?scope=<?= h($scopeKey) ?>&mode=capture"><?= h($scopeLabel) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<form method="post" action="/ledger/" class="mobile-form-grid">
|
||||
<input type="hidden" name="action" value="bulk-record-coffee">
|
||||
<input type="hidden" name="scope" value="<?= h($ledgerScope) ?>">
|
||||
<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>
|
||||
<div class="mobile-entry-list">
|
||||
<?php foreach ($ledgerMembers as $member): ?>
|
||||
<label class="mobile-entry-row">
|
||||
<div>
|
||||
<strong><?= h((string) ($member['display_name'] ?? '')) ?></strong>
|
||||
<span><?= num($member['recent_strokes'] ?? 0) ?> Striche in 100 Tagen</span>
|
||||
</div>
|
||||
<input type="number" name="strokes[<?= h((string) ($member['id'] ?? '')) ?>]" min="0" step="1" value="0">
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="mobile-sticky-submit">
|
||||
<span class="muted">Lokaler Batch fuer alle Werte > 0</span>
|
||||
<button type="submit">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<section class="card mobile-journal-card">
|
||||
<div class="bulk-summary">
|
||||
<span class="bulk-chip"><?= num(count($ledger)) ?> Einträge</span>
|
||||
<span>Prüfen und korrigieren</span>
|
||||
</div>
|
||||
<div class="mobile-journal-list">
|
||||
<?php foreach ($ledger as $entry): ?>
|
||||
<article class="mobile-journal-item">
|
||||
<div class="mobile-journal-item__head">
|
||||
<div>
|
||||
<strong><?= h((string) ($entry['member_name'] ?? '')) ?></strong>
|
||||
<div class="mobile-journal-item__meta">
|
||||
<span><?= h((string) ($entry['entry_type'] ?? '')) ?> · <?= h((string) ($entry['reference_type'] ?? '')) ?></span>
|
||||
<span><?= dt((string) ($entry['booked_at'] ?? '')) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<strong><?= money($entry['amount'] ?? 0) ?></strong>
|
||||
</div>
|
||||
<?php if ((string) ($entry['reference_type'] ?? '') === 'coffee_entry' && (string) ($entry['reference_id'] ?? '') !== ''): ?>
|
||||
<form method="post" action="/ledger/">
|
||||
<input type="hidden" name="action" value="delete-coffee">
|
||||
<input type="hidden" name="reference_id" value="<?= h((string) ($entry['reference_id'] ?? '')) ?>">
|
||||
<button type="submit" class="button secondary">Löschen</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="desktop-ledger-shell">
|
||||
<section class="hero"><div class="eyebrow">Buchungen</div><h1>Striche und Buchungen</h1><p>Kaffeeeinträge und alle zugehörigen Buchungen bleiben tenantweise nachvollziehbar.</p></section>
|
||||
<section class="card bulk-shell">
|
||||
<div class="bulk-toolbar">
|
||||
<div>
|
||||
<h2>Sammelerfassung wie in der Kaffeeliste</h2>
|
||||
<div class="bulk-summary">
|
||||
<span class="bulk-chip"><?= num(count($ledgerMembers)) ?> Teilnehmer</span>
|
||||
<span>Ansicht: <?= h(['all' => 'Alle', 'front' => 'Vorderseite', 'back' => 'Rückseite'][$ledgerScope] ?? 'Alle') ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<?php foreach (['all' => 'Alle', 'front' => 'Vorderseite', 'back' => 'Rückseite'] as $scopeKey => $scopeLabel): ?>
|
||||
<a class="button <?= $ledgerScope === $scopeKey ? '' : 'secondary' ?>" href="/ledger/?scope=<?= h($scopeKey) ?>"><?= h($scopeLabel) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/ledger/" class="stack">
|
||||
<input type="hidden" name="action" value="bulk-record-coffee">
|
||||
<input type="hidden" name="scope" value="<?= h((string) ($_GET['scope'] ?? 'all')) ?>">
|
||||
<div class="grid">
|
||||
<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>
|
||||
</div>
|
||||
<div class="table">
|
||||
<table>
|
||||
<thead><tr><th>Mitglied</th><th>Striche</th><th>100 Tage</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($ledgerMembers as $member): ?>
|
||||
<tr>
|
||||
<td><strong><?= h((string) ($member['display_name'] ?? '')) ?></strong></td>
|
||||
<td><input type="number" name="strokes[<?= h((string) ($member['id'] ?? '')) ?>]" min="0" step="1" value="0"></td>
|
||||
<td><?= num($member['recent_strokes'] ?? 0) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="bulk-footer"><span class="muted">Die Eingabe gilt nur für Werte größer als 0.</span><button type="submit">Sammel-Striche eintragen</button></div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="card" style="margin-top:18px">
|
||||
<h2>Einzelbuchung</h2>
|
||||
<form method="post" action="/ledger/" 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']) ?>"><?= h((string) $member['display_name']) ?></option><?php endforeach; ?></select></label>
|
||||
@@ -1053,25 +1291,62 @@ $marketing = app_marketing_messages();
|
||||
<label>Buchungszeit<input type="datetime-local" name="booked_at" value="<?= date('Y-m-d\TH:i') ?>"></label>
|
||||
<div class="actions"><button type="submit">Buchen</button></div>
|
||||
</form>
|
||||
</article>
|
||||
<article class="card"><h2>Was dieser Bereich abdeckt</h2><ul class="list"><li>Einzel- und Sammelbuchungen laufen in einem gemeinsamen Ablauf zusammen.</li><li>Jeder Verbrauch erzeugt automatisch den passenden Ledger-Eintrag.</li><li>Die letzten Buchungen bleiben je Tenant nachvollziehbar.</li></ul></article>
|
||||
</section>
|
||||
<section class="card" style="margin-top:18px"><div class="table"><table><thead><tr><th>Zeit</th><th>Mitglied</th><th>Typ</th><th>Referenz</th><th>Betrag</th><th>Aktion</th></tr></thead><tbody><?php foreach ($ledger as $entry): ?><tr><td><?= dt((string) ($entry['booked_at'] ?? '')) ?></td><td><?= h((string) ($entry['member_name'] ?? '')) ?></td><td><?= h((string) ($entry['entry_type'] ?? '')) ?></td><td><?= h((string) ($entry['reference_type'] ?? '')) ?></td><td><?= money($entry['amount'] ?? 0) ?></td><td><?php if ((string) ($entry['reference_type'] ?? '') === 'coffee_entry' && (string) ($entry['reference_id'] ?? '') !== ''): ?><form method="post" action="/ledger/"><input type="hidden" name="action" value="delete-coffee"><input type="hidden" name="reference_id" value="<?= h((string) ($entry['reference_id'] ?? '')) ?>"><button type="submit" class="button secondary">Löschen</button></form><?php else: ?><span class="muted">-</span><?php endif; ?></td></tr><?php endforeach; ?></tbody></table></div></section>
|
||||
</section>
|
||||
<section class="card" style="margin-top:18px"><div class="table"><table><thead><tr><th>Zeit</th><th>Mitglied</th><th>Typ</th><th>Referenz</th><th>Betrag</th><th>Aktion</th></tr></thead><tbody><?php foreach ($ledger as $entry): ?><tr><td><?= dt((string) ($entry['booked_at'] ?? '')) ?></td><td><?= h((string) ($entry['member_name'] ?? '')) ?></td><td><?= h((string) ($entry['entry_type'] ?? '')) ?></td><td><?= h((string) ($entry['reference_type'] ?? '')) ?></td><td><?= money($entry['amount'] ?? 0) ?></td><td><?php if ((string) ($entry['reference_type'] ?? '') === 'coffee_entry' && (string) ($entry['reference_id'] ?? '') !== ''): ?><form method="post" action="/ledger/"><input type="hidden" name="action" value="delete-coffee"><input type="hidden" name="reference_id" value="<?= h((string) ($entry['reference_id'] ?? '')) ?>"><button type="submit" class="button secondary">Löschen</button></form><?php else: ?><span class="muted">-</span><?php endif; ?></td></tr><?php endforeach; ?></tbody></table></div></section>
|
||||
</div>
|
||||
<?php elseif ($page === 'payments'): ?>
|
||||
<section class="hero"><div class="eyebrow">Einzahlungen</div><h1>Zahlungen</h1><p>Einzahlungen werden direkt in Zahlungstabelle und Ledger geschrieben.</p></section>
|
||||
<section class="grid grid-2">
|
||||
<article class="card">
|
||||
<h2>Einzahlung buchen</h2>
|
||||
<form method="post" action="/payments/" 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']) ?>"><?= 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>
|
||||
<section class="card bulk-shell">
|
||||
<div class="bulk-toolbar">
|
||||
<div>
|
||||
<h2>Sammel-Einzahlungen</h2>
|
||||
<div class="bulk-summary">
|
||||
<span class="bulk-chip"><?= num(count($paymentMembers)) ?> Teilnehmer</span>
|
||||
<span>Ansicht: <?= h(['all' => 'Alle', 'front' => 'Vorderseite', 'back' => 'Rückseite'][$paymentScope] ?? 'Alle') ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<?php foreach (['all' => 'Alle', 'front' => 'Vorderseite', 'back' => 'Rückseite'] as $scopeKey => $scopeLabel): ?>
|
||||
<a class="button <?= $paymentScope === $scopeKey ? '' : 'secondary' ?>" href="/payments/?scope=<?= h($scopeKey) ?>"><?= h($scopeLabel) ?></a>
|
||||
<?php endforeach; ?>
|
||||
<a class="button secondary" href="/imports/">CSV-Import</a>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/payments/" class="stack">
|
||||
<input type="hidden" name="action" value="bulk-record-payment">
|
||||
<input type="hidden" name="scope" value="<?= h((string) ($_GET['scope'] ?? 'all')) ?>">
|
||||
<div class="grid">
|
||||
<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 speichern</button></div>
|
||||
</form>
|
||||
</article>
|
||||
<article class="card"><h2>Wofür dieser Bereich da ist</h2><ul class="list"><li>Manuelle Einzahlungen sind direkt erfasst.</li><li>PayPal oder Bank können getrennt ausgewiesen werden.</li><li>Jede Zahlung erscheint sofort im Ledger.</li></ul><p style="margin-top:14px"><?= h((string) ($tenantSettings['payment_hint'] ?? '')) ?></p></article>
|
||||
</div>
|
||||
<div class="table">
|
||||
<table>
|
||||
<thead><tr><th>Mitglied</th><th>Betrag</th><th>100 Tage</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($paymentMembers as $member): ?>
|
||||
<tr>
|
||||
<td><strong><?= h((string) ($member['display_name'] ?? '')) ?></strong></td>
|
||||
<td><input type="number" name="amounts[<?= h((string) ($member['id'] ?? '')) ?>]" min="0" step="0.01" value="0.00"></td>
|
||||
<td><?= num($member['recent_strokes'] ?? 0) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="bulk-footer"><span class="muted">Die Eingabe gilt nur für Werte größer als 0.</span><button type="submit">Sammel-Einzahlungen eintragen</button></div>
|
||||
</form>
|
||||
<p><?= h((string) ($tenantSettings['payment_hint'] ?? '')) ?></p>
|
||||
</section>
|
||||
<section class="card" style="margin-top:18px">
|
||||
<h2>Einzelzahlung</h2>
|
||||
<form method="post" action="/payments/" 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']) ?>"><?= 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 speichern</button></div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="card" style="margin-top:18px"><div class="table"><table><thead><tr><th>Zeit</th><th>Mitglied</th><th>Methode</th><th>Betrag</th><th>Aktion</th></tr></thead><tbody><?php foreach ($payments as $entry): ?><tr><td><?= dt((string) ($entry['booked_at'] ?? '')) ?></td><td><?= h((string) ($entry['member_name'] ?? '')) ?></td><td><?= h((string) ($entry['payment_method'] ?? '')) ?></td><td><?= money($entry['amount'] ?? 0) ?></td><td><form method="post" action="/payments/"><input type="hidden" name="action" value="delete-payment"><input type="hidden" name="payment_id" value="<?= h((string) ($entry['id'] ?? '')) ?>"><button type="submit" class="button secondary">Stornieren</button></form></td></tr><?php endforeach; ?></tbody></table></div></section>
|
||||
<?php elseif ($page === 'content'): ?>
|
||||
@@ -1135,14 +1410,34 @@ $marketing = app_marketing_messages();
|
||||
</form>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>Aktuelle Lizenz</h2>
|
||||
<h2>Anzeige</h2>
|
||||
<div class="stack">
|
||||
<div class="metric"><h3><?= h((string) ($tenantLicense['plan_name'] ?? 'Free')) ?></h3><p>Freigeschalteter Lizenzplan für diesen Mandanten.</p></div>
|
||||
<div class="metric"><h3>Mitgliederrahmen</h3><p><?= isset($tenantLicense['member_limit']) && (int) $tenantLicense['member_limit'] > 0 ? h((string) $tenantLicense['member_limit']) . ' aktive Mitglieder im Standardumfang.' : 'Individuell vereinbart.' ?></p></div>
|
||||
<div class="metric"><h3>Freigeschaltete Kernfunktionen</h3><ul class="list"><?php foreach (['tenant_settings' => 'Mandanten-Einstellungen', 'pdf_export' => 'PDF-Listen', 'paper_strike_entry' => 'Papierlisten-Erfassung'] as $featureKey => $featureLabel): ?><?php if (!empty($tenantLicense['features'][$featureKey])): ?><li><?= h($featureLabel) ?></li><?php endif; ?><?php endforeach; ?></ul></div>
|
||||
<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>
|
||||
</article>
|
||||
</section>
|
||||
<section class="card" style="margin-top:18px">
|
||||
<h2>Daten, Hilfe und Kommunikation</h2>
|
||||
<div class="grid grid-3">
|
||||
<div class="metric"><h3>Daten</h3><p>Importe, Exporte und Reporting sind im Menü jetzt unter einem gemeinsamen Aufgabenraum gebündelt.</p></div>
|
||||
<div class="metric"><h3>Hilfe & Inhalte</h3><p>Hinweise, FAQ, Support und Umfragen sind zusammen erreichbar, statt als verstreute Einzelpunkte aufzutauchen.</p></div>
|
||||
<div class="metric"><h3>Verwaltung</h3><p>Rollen und Mandanten-Einstellungen bleiben zusammen im Verwaltungsbereich.</p></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card" style="margin-top:18px">
|
||||
<h2>Aktuelle Lizenz</h2>
|
||||
<div class="stack">
|
||||
<div class="metric"><h3><?= h((string) ($tenantLicense['plan_name'] ?? 'Free')) ?></h3><p>Freigeschalteter Lizenzplan für diesen Mandanten.</p></div>
|
||||
<div class="metric"><h3>Mitgliederrahmen</h3><p><?= isset($tenantLicense['member_limit']) && (int) $tenantLicense['member_limit'] > 0 ? h((string) $tenantLicense['member_limit']) . ' aktive Mitglieder im Standardumfang.' : 'Individuell vereinbart.' ?></p></div>
|
||||
<div class="metric"><h3>Freigeschaltete Kernfunktionen</h3><ul class="list"><?php foreach (['tenant_settings' => 'Mandanten-Einstellungen', 'pdf_export' => 'PDF-Listen', 'paper_strike_entry' => 'Papierlisten-Erfassung'] as $featureKey => $featureLabel): ?><?php if (!empty($tenantLicense['features'][$featureKey])): ?><li><?= h($featureLabel) ?></li><?php endif; ?><?php endforeach; ?></ul></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card" style="margin-top:18px">
|
||||
<h2>Farben im Tenant</h2>
|
||||
<div class="grid grid-4">
|
||||
@@ -1280,7 +1575,87 @@ $marketing = app_marketing_messages();
|
||||
|
||||
<p class="footer">Die Kaffeeliste</p>
|
||||
</div>
|
||||
<?php if ($auth !== null && $mobileBottomNavItems !== []): ?>
|
||||
<nav class="mobile-bottom-nav" aria-label="Mobile Kernnavigation">
|
||||
<?php foreach ($mobileBottomNavItems as $item): ?>
|
||||
<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',
|
||||
default => '•',
|
||||
} ?></span>
|
||||
<span class="mobile-bottom-nav__label"><?= h((string) ($item['label'] ?? 'Link')) ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<button type="button" class="mobile-bottom-nav__toggle" data-mobile-drawer-open>
|
||||
<span class="mobile-bottom-nav__icon">+</span>
|
||||
<span class="mobile-bottom-nav__label">Mehr</span>
|
||||
</button>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
<?php endif; ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const details = document.querySelectorAll('.site-more');
|
||||
const drawer = document.querySelector('[data-mobile-drawer]');
|
||||
const drawerOpeners = document.querySelectorAll('[data-mobile-drawer-open]');
|
||||
const drawerClosers = document.querySelectorAll('[data-mobile-drawer-close]');
|
||||
|
||||
function closeOthers(current) {
|
||||
details.forEach(function (other) {
|
||||
if (other !== current) {
|
||||
other.open = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
details.forEach(function (detail) {
|
||||
const summary = detail.querySelector('summary');
|
||||
if (summary) {
|
||||
summary.addEventListener('click', function () {
|
||||
if (!detail.open) {
|
||||
closeOthers(detail);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
detail.addEventListener('toggle', function () {
|
||||
if (detail.open) {
|
||||
closeOthers(detail);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.site-more__panel a, .mobile-drawer__link').forEach(function (link) {
|
||||
link.addEventListener('click', function () {
|
||||
details.forEach(function (detail) {
|
||||
detail.open = false;
|
||||
});
|
||||
if (drawer) {
|
||||
drawer.classList.remove('is-open');
|
||||
document.body.classList.remove('mobile-drawer-open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (drawer) {
|
||||
drawerOpeners.forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
drawer.classList.add('is-open');
|
||||
document.body.classList.add('mobile-drawer-open');
|
||||
});
|
||||
});
|
||||
|
||||
drawerClosers.forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
drawer.classList.remove('is-open');
|
||||
document.body.classList.remove('mobile-drawer-open');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -99,6 +99,15 @@ $priorities = $pageData['priorities'] ?? [];
|
||||
$routeTargets = $pageData['route_targets'] ?? [];
|
||||
$selectedRequestId = (string) ($_GET['request'] ?? '');
|
||||
$tenantNavItems = app_tenant_navigation_items($auth, $tenantLicense);
|
||||
$tenantNavGroups = app_tenant_navigation_groups($tenantNavItems);
|
||||
$tenantHeaderNavItems = [];
|
||||
$mobileBottomNavItems = [];
|
||||
foreach ($tenantNavItems as $item) {
|
||||
if (in_array((string) ($item['key'] ?? ''), ['dashboard', 'members', 'ledger', 'payments'], true)) {
|
||||
$tenantHeaderNavItems[] = $item;
|
||||
$mobileBottomNavItems[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$template = dirname(__DIR__, 2) . '/resources/views/support/index.blade.php';
|
||||
if (!is_file($template)) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -67,6 +67,15 @@ $tenantSnapshots = $overview['tenant_snapshots'] ?? [];
|
||||
$permissionGroups = $overview['permission_groups'] ?? [];
|
||||
$notes = $overview['notes'] ?? [];
|
||||
$tenantNavItems = app_tenant_navigation_items($auth, $tenantLicense);
|
||||
$tenantNavGroups = app_tenant_navigation_groups($tenantNavItems);
|
||||
$tenantHeaderNavItems = [];
|
||||
$mobileBottomNavItems = [];
|
||||
foreach ($tenantNavItems as $item) {
|
||||
if (in_array((string) ($item['key'] ?? ''), ['dashboard', 'members', 'ledger', 'payments'], true)) {
|
||||
$tenantHeaderNavItems[] = $item;
|
||||
$mobileBottomNavItems[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$template = dirname(__DIR__, 3) . '/resources/views/tenants/roles.blade.php';
|
||||
if (!is_file($template)) {
|
||||
|
||||
@@ -20,12 +20,16 @@
|
||||
$layoutNavItems = function_exists('app_tenant_navigation_items')
|
||||
? app_tenant_navigation_items($layoutAuth, $layoutLicense)
|
||||
: [];
|
||||
$layoutNavGroups = function_exists('app_tenant_navigation_groups')
|
||||
? app_tenant_navigation_groups($layoutNavItems)
|
||||
: [];
|
||||
$layoutGuestItems = [
|
||||
['href' => '/', 'label' => 'Start'],
|
||||
['href' => '/login/', 'label' => 'Anmeldung'],
|
||||
['href' => '/admin/login/', 'label' => 'Admin'],
|
||||
];
|
||||
$layoutPrimaryNavItems = $layoutNavItems !== [] ? $layoutNavItems : $layoutGuestItems;
|
||||
$layoutHeaderNavItems = $layoutPrimaryNavItems;
|
||||
$layoutCurrentLabel = 'Start';
|
||||
foreach ($layoutPrimaryNavItems as $item) {
|
||||
$itemHref = rtrim((string) ($item['href'] ?? '/'), '/');
|
||||
@@ -38,6 +42,17 @@
|
||||
$layoutThemeCss = function_exists('app_tenant_theme_root_css')
|
||||
? app_tenant_theme_root_css($layoutThemeSettings)
|
||||
: '';
|
||||
if (is_array($layoutAuth)) {
|
||||
$layoutMobilePrimaryNavItems = array_slice($layoutHeaderNavItems, 0, 3);
|
||||
} else {
|
||||
$layoutMobilePrimaryNavItems = array_slice($layoutPrimaryNavItems, 0, 2);
|
||||
}
|
||||
$layoutMobileDrawerPrimaryItems = array_slice($layoutPrimaryNavItems, count($layoutMobilePrimaryNavItems));
|
||||
$layoutMobileHasDrawer = $layoutMobileDrawerPrimaryItems !== [] || $layoutNavGroups !== [] || is_array($layoutAuth);
|
||||
$layoutMobileNavColumns = count($layoutMobilePrimaryNavItems) + ($layoutMobileHasDrawer ? 1 : 0);
|
||||
if ($layoutMobileNavColumns < 3) {
|
||||
$layoutMobileNavColumns = 3;
|
||||
}
|
||||
@endphp
|
||||
@php
|
||||
$resolvedLayoutMode = trim((string) $__env->yieldContent('layout_mode', 'app'));
|
||||
@@ -122,6 +137,7 @@
|
||||
.site-shell {
|
||||
min-height: 100vh;
|
||||
padding: 0 0 24px;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.site-header,
|
||||
@@ -138,8 +154,8 @@
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 30;
|
||||
width: 100vw;
|
||||
margin-left: calc(50% - 50vw);
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@@ -289,70 +305,205 @@
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.mobile-nav {
|
||||
.mobile-bottom-nav {
|
||||
display: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mobile-nav[open] { z-index: 40; }
|
||||
.mobile-bottom-nav__bar {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.mobile-nav__toggle {
|
||||
.mobile-bottom-nav__link,
|
||||
.mobile-bottom-nav__more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
min-height: 38px;
|
||||
padding: 0.45rem 0.72rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--line-strong);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--text-strong);
|
||||
font-size: 0.92rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mobile-nav__toggle::-webkit-details-marker { display: none; }
|
||||
|
||||
.mobile-nav__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-nav__panel {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 12px);
|
||||
width: min(320px, calc(100vw - 32px));
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
min-height: 56px;
|
||||
padding: 8px 6px 7px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--surface-3);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--text-soft);
|
||||
font-weight: 600;
|
||||
font-size: 0.74rem;
|
||||
line-height: 1.15;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mobile-bottom-nav__link:hover,
|
||||
.mobile-bottom-nav__more:hover {
|
||||
text-decoration: none;
|
||||
color: var(--text-strong);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.mobile-bottom-nav__link.is-active {
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
background: linear-gradient(135deg, #587aff 0%, #243a91 100%);
|
||||
}
|
||||
|
||||
.mobile-bottom-nav__label {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-bottom-nav__meta {
|
||||
display: block;
|
||||
color: var(--text-faint);
|
||||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mobile-drawer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: none;
|
||||
z-index: 70;
|
||||
}
|
||||
|
||||
.mobile-drawer.is-open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mobile-drawer__scrim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 0;
|
||||
background: rgba(4, 10, 18, 0.62);
|
||||
backdrop-filter: blur(8px);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mobile-drawer__panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: min(94vw, 408px);
|
||||
max-width: 100%;
|
||||
padding: 18px 16px calc(26px + env(safe-area-inset-bottom));
|
||||
border-left: 1px solid var(--line);
|
||||
background: linear-gradient(180deg, rgba(13, 18, 31, 0.99), rgba(8, 10, 18, 0.98));
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.mobile-nav__stack {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.mobile-nav__stack .top-nav__link {
|
||||
justify-content: flex-start;
|
||||
padding: 0.78rem 0.92rem;
|
||||
.mobile-drawer__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mobile-drawer__title {
|
||||
margin: 0;
|
||||
font-size: 1.08rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.mobile-drawer__copy {
|
||||
margin: 4px 0 0;
|
||||
color: var(--text-soft);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.mobile-drawer__close {
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
padding: 0.45rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.mobile-drawer__sections {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
overflow: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.mobile-drawer__section {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile-drawer__label {
|
||||
font-size: 0.74rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.mobile-drawer__links {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile-drawer__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0.8rem 0.95rem;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--text-strong);
|
||||
font-size: 0.93rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mobile-nav__footer {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
.mobile-drawer__link::after {
|
||||
content: "›";
|
||||
color: var(--text-faint);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.mobile-drawer__link:hover {
|
||||
text-decoration: none;
|
||||
border-color: rgba(255, 255, 255, 0.14);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.mobile-drawer__link.is-active {
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
background: linear-gradient(135deg, #587aff 0%, #243a91 100%);
|
||||
}
|
||||
|
||||
.mobile-drawer__link.is-active::after {
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.mobile-drawer__empty {
|
||||
padding: 12px 14px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
color: var(--text-soft);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.mobile-drawer__footer {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.button,
|
||||
@@ -662,6 +813,7 @@
|
||||
min-height: 100vh;
|
||||
padding: 0 0 92px;
|
||||
gap: 12px;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.marketing-main {
|
||||
@@ -679,8 +831,8 @@
|
||||
gap: 14px;
|
||||
height: 88px;
|
||||
min-height: 88px;
|
||||
width: 100vw;
|
||||
margin-left: calc(50% - 50vw);
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-bottom: 20px;
|
||||
padding: 0 18px;
|
||||
border: 0;
|
||||
@@ -804,7 +956,7 @@
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 10px);
|
||||
width: min(280px, calc(100vw - 24px));
|
||||
width: min(280px, calc(100% - 24px));
|
||||
padding: 14px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(163, 183, 255, 0.14);
|
||||
@@ -861,24 +1013,51 @@
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.site-shell {
|
||||
padding: 0 0 18px;
|
||||
padding: 0 0 104px;
|
||||
}
|
||||
|
||||
.site-header__inner {
|
||||
width: 100%;
|
||||
min-height: 68px;
|
||||
padding: 0 16px;
|
||||
padding: 0 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-nav { display: none; }
|
||||
.site-actions { display: none; }
|
||||
.app-shell {
|
||||
width: calc(100% - 20px);
|
||||
margin: 10px auto;
|
||||
min-height: calc(100vh - 20px);
|
||||
width: calc(100% - 16px);
|
||||
margin: 8px auto;
|
||||
min-height: calc(100vh - 16px);
|
||||
}
|
||||
|
||||
.mobile-bottom-nav {
|
||||
display: block;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 50;
|
||||
padding: 8px 10px calc(8px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid rgba(137, 154, 188, 0.14);
|
||||
background: rgba(8, 10, 18, 0.9);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.mobile-bottom-nav__bar {
|
||||
grid-template-columns: repeat(var(--mobile-nav-columns, 4), minmax(0, 1fr));
|
||||
}
|
||||
|
||||
body.mobile-drawer-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-drawer__panel {
|
||||
width: min(96vw, 420px);
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
}
|
||||
|
||||
.mobile-nav { display: block; }
|
||||
.grid--2,
|
||||
.grid--3,
|
||||
.grid--4,
|
||||
@@ -892,26 +1071,26 @@
|
||||
table { min-width: 0; }
|
||||
.marketing-shell { padding: 0 0 88px; }
|
||||
.marketing-main {
|
||||
width: calc(100% - 24px);
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
.marketing-bar {
|
||||
height: 72px;
|
||||
min-height: 72px;
|
||||
margin-bottom: 16px;
|
||||
padding: 0 12px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.marketing-nav,
|
||||
.marketing-actions { display: none; }
|
||||
.marketing-mobile { display: block; }
|
||||
.marketing-footer {
|
||||
padding: 10px 12px;
|
||||
padding: 10px 10px;
|
||||
}
|
||||
.marketing-footer__inner {
|
||||
width: 100%;
|
||||
}
|
||||
.site-main,
|
||||
.app-footer__inner {
|
||||
width: calc(100% - 20px);
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
@@ -986,7 +1165,7 @@
|
||||
</a>
|
||||
|
||||
<nav class="top-nav" aria-label="Hauptnavigation">
|
||||
@foreach ($layoutPrimaryNavItems as $item)
|
||||
@foreach ($layoutHeaderNavItems as $item)
|
||||
@php
|
||||
$itemHref = rtrim((string) ($item['href'] ?? '/'), '/');
|
||||
$itemHref = $itemHref === '' ? '/' : $itemHref;
|
||||
@@ -1005,29 +1184,6 @@
|
||||
@else
|
||||
<a class="button button--ghost" href="/login/">Anmelden</a>
|
||||
@endif
|
||||
|
||||
<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">
|
||||
@foreach ($layoutPrimaryNavItems 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
|
||||
</nav>
|
||||
@if (is_array($layoutAuth))
|
||||
<div class="mobile-nav__footer">
|
||||
<form method="post" action="/logout/">
|
||||
<button type="submit" class="button button--ghost">Abmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1042,7 +1198,173 @@
|
||||
<span>Klare Bereiche mit Navigation oben</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div class="mobile-bottom-nav" style="--mobile-nav-columns: {{ $layoutMobileNavColumns }};">
|
||||
<nav class="mobile-bottom-nav__bar" aria-label="Mobile Navigation">
|
||||
@foreach ($layoutMobilePrimaryNavItems as $item)
|
||||
@php
|
||||
$itemHref = rtrim((string) ($item['href'] ?? '/'), '/');
|
||||
$itemHref = $itemHref === '' ? '/' : $itemHref;
|
||||
$isActive = $layoutPath === $itemHref;
|
||||
@endphp
|
||||
<a href="{{ $item['href'] ?? '/' }}" class="mobile-bottom-nav__link {{ $isActive ? 'is-active' : '' }}" @if ($isActive) aria-current="page" @endif>
|
||||
<span class="mobile-bottom-nav__label">{{ $item['label'] ?? 'Link' }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
|
||||
@if ($layoutMobileHasDrawer)
|
||||
<button type="button" class="mobile-bottom-nav__more" data-mobile-drawer-open>
|
||||
<span class="mobile-bottom-nav__label">Mehr</span>
|
||||
<span class="mobile-bottom-nav__meta">Menü</span>
|
||||
</button>
|
||||
@endif
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@if ($layoutMobileHasDrawer)
|
||||
<div class="mobile-drawer" data-mobile-drawer>
|
||||
<button type="button" class="mobile-drawer__scrim" aria-label="Menü schließen" data-mobile-drawer-close></button>
|
||||
<aside class="mobile-drawer__panel" role="dialog" aria-label="Weitere Navigation">
|
||||
<div class="mobile-drawer__header">
|
||||
<div>
|
||||
<p class="mobile-drawer__title">Mehr Bereiche</p>
|
||||
<p class="mobile-drawer__copy">
|
||||
@if (is_array($layoutAuth))
|
||||
{{ $layoutAuth['tenant_name'] ?? 'Tenant' }} · {{ $layoutCurrentLabel }}
|
||||
@else
|
||||
Zusätzliche Links und Zugangsmöglichkeiten
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="button button--ghost mobile-drawer__close" data-mobile-drawer-close aria-label="Schließen">×</button>
|
||||
</div>
|
||||
|
||||
<div class="mobile-drawer__sections">
|
||||
@if ($layoutMobileDrawerPrimaryItems !== [])
|
||||
<section class="mobile-drawer__section">
|
||||
<div class="mobile-drawer__label">Weitere Seiten</div>
|
||||
<div class="mobile-drawer__links">
|
||||
@foreach ($layoutMobileDrawerPrimaryItems as $item)
|
||||
@php
|
||||
$itemHref = rtrim((string) ($item['href'] ?? '/'), '/');
|
||||
$itemHref = $itemHref === '' ? '/' : $itemHref;
|
||||
$isActive = $layoutPath === $itemHref;
|
||||
@endphp
|
||||
<a href="{{ $item['href'] ?? '/' }}" class="mobile-drawer__link {{ $isActive ? 'is-active' : '' }}" @if ($isActive) aria-current="page" @endif>{{ $item['label'] ?? 'Link' }}</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@foreach ($layoutNavGroups as $group)
|
||||
<section class="mobile-drawer__section">
|
||||
<div class="mobile-drawer__label">{{ $group['label'] ?? 'Mehr' }}</div>
|
||||
<div class="mobile-drawer__links">
|
||||
@foreach (($group['items'] ?? []) as $item)
|
||||
@php
|
||||
$itemHref = rtrim((string) ($item['href'] ?? '/'), '/');
|
||||
$itemHref = $itemHref === '' ? '/' : $itemHref;
|
||||
$isActive = $layoutPath === $itemHref;
|
||||
@endphp
|
||||
<a href="{{ $item['href'] ?? '/' }}" class="mobile-drawer__link {{ $isActive ? 'is-active' : '' }}" @if ($isActive) aria-current="page" @endif>{{ $item['label'] ?? 'Link' }}</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
@endforeach
|
||||
|
||||
<section class="mobile-drawer__section">
|
||||
<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>
|
||||
</div>
|
||||
@else
|
||||
<div class="mobile-drawer__links">
|
||||
<a class="mobile-drawer__link" href="/login/">Anmelden</a>
|
||||
<a class="mobile-drawer__link" href="/admin/login/">Verwaltung</a>
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="mobile-drawer__footer">
|
||||
@if (is_array($layoutAuth))
|
||||
<form method="post" action="/logout/">
|
||||
<button type="submit" class="button button--ghost" style="width: 100%;">Abmelden</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const detailGroups = document.querySelectorAll('.top-nav__group');
|
||||
const drawer = document.querySelector('[data-mobile-drawer]');
|
||||
const drawerOpeners = document.querySelectorAll('[data-mobile-drawer-open]');
|
||||
const drawerClosers = document.querySelectorAll('[data-mobile-drawer-close]');
|
||||
|
||||
function closeOthers(current) {
|
||||
detailGroups.forEach(function (other) {
|
||||
if (other !== current) {
|
||||
other.open = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
detailGroups.forEach(function (detail) {
|
||||
const summary = detail.querySelector('summary');
|
||||
if (summary) {
|
||||
summary.addEventListener('click', function () {
|
||||
if (!detail.open) {
|
||||
closeOthers(detail);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
detail.addEventListener('toggle', function () {
|
||||
if (detail.open) {
|
||||
closeOthers(detail);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function closeDrawer() {
|
||||
if (drawer) {
|
||||
drawer.classList.remove('is-open');
|
||||
document.body.classList.remove('mobile-drawer-open');
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.top-nav__panel a, .mobile-drawer__link').forEach(function (link) {
|
||||
link.addEventListener('click', function () {
|
||||
detailGroups.forEach(function (detail) {
|
||||
detail.open = false;
|
||||
});
|
||||
closeDrawer();
|
||||
});
|
||||
});
|
||||
|
||||
drawerOpeners.forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
detailGroups.forEach(function (detail) {
|
||||
detail.open = false;
|
||||
});
|
||||
if (drawer) {
|
||||
drawer.classList.add('is-open');
|
||||
document.body.classList.add('mobile-drawer-open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
drawerClosers.forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
closeDrawer();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -103,6 +103,12 @@
|
||||
}
|
||||
.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-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::-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)}
|
||||
.hero,.card,.alert{
|
||||
border:1px solid rgba(163,183,255,.12);
|
||||
border-radius:16px;
|
||||
@@ -115,23 +121,22 @@
|
||||
color:#f4f7ff;
|
||||
}
|
||||
.muted,p{color:rgba(219,228,246,.74)}
|
||||
.site-mobile{display:none;position:relative}
|
||||
.site-mobile[open]{z-index:20}
|
||||
.site-toggle{
|
||||
.site-mobile{display:none}
|
||||
.site-mobile__toggle{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap:12px;
|
||||
justify-content:center;
|
||||
gap:10px;
|
||||
cursor:pointer;
|
||||
list-style:none;
|
||||
padding:10px 12px;
|
||||
min-height:38px;
|
||||
padding:.45rem .72rem;
|
||||
border-radius:10px;
|
||||
border:1px solid rgba(201,214,255,.14);
|
||||
background:rgba(255,255,255,.04);
|
||||
color:#f4f7ff;
|
||||
font-weight:700;
|
||||
}
|
||||
.site-toggle::-webkit-details-marker{display:none}
|
||||
.site-toggle::before{
|
||||
.site-mobile__toggle::before{
|
||||
content:"";
|
||||
display:block;
|
||||
width:18px;
|
||||
@@ -140,20 +145,76 @@
|
||||
background:currentColor;
|
||||
box-shadow:0 -6px 0 currentColor,0 6px 0 currentColor;
|
||||
}
|
||||
.site-panel{
|
||||
.mobile-drawer{position:fixed;inset:0;display:none;z-index:55}
|
||||
.mobile-drawer.is-open{display:block}
|
||||
.mobile-drawer__scrim{position:absolute;inset:0;background:rgba(4,10,18,.58);backdrop-filter:blur(6px)}
|
||||
.mobile-drawer__panel{
|
||||
position:absolute;
|
||||
right:0;
|
||||
top:calc(100% + 12px);
|
||||
width:min(320px,calc(100vw - 32px));
|
||||
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);
|
||||
top:0;
|
||||
bottom:0;
|
||||
width:min(92vw,380px);
|
||||
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);
|
||||
display:grid;
|
||||
grid-template-rows:auto 1fr auto;
|
||||
gap:18px;
|
||||
overflow:auto;
|
||||
}
|
||||
.site-stack{display:grid;gap:10px}
|
||||
.site-stack .site-nav__link{justify-content:flex-start;padding:.6rem .75rem;background:rgba(255,255,255,.04);border-color:rgba(201,214,255,.1);color:#f4f7ff}
|
||||
.site-footer-actions{display:flex;justify-content:flex-end;margin-top:12px}
|
||||
.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__close{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
width:40px;
|
||||
height:40px;
|
||||
border-radius:999px;
|
||||
border:1px solid rgba(163,183,255,.12);
|
||||
background:rgba(255,255,255,.04);
|
||||
color:#f4f7ff;
|
||||
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__links{display:grid;gap:8px}
|
||||
.mobile-drawer__link{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
padding:.9rem 1rem;
|
||||
border-radius:14px;
|
||||
border:1px solid rgba(163,183,255,.12);
|
||||
background:rgba(255,255,255,.04);
|
||||
color:#f4f7ff;
|
||||
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__meta{display:grid;gap:4px}
|
||||
.mobile-bottom-nav{display:none}
|
||||
.mobile-bottom-nav__link,.mobile-bottom-nav__toggle{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
gap:6px;
|
||||
min-height:56px;
|
||||
padding:0 8px;
|
||||
border:0;
|
||||
background:transparent;
|
||||
color:rgba(231,238,255,.8);
|
||||
font:inherit;
|
||||
font-weight:700;
|
||||
}
|
||||
.mobile-bottom-nav__link.active,.mobile-bottom-nav__toggle.active{color:#f4f7ff}
|
||||
.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}
|
||||
.hero{
|
||||
padding:24px;
|
||||
@@ -251,15 +312,34 @@
|
||||
.status.is-closed::before{background:#11623d}
|
||||
.mono{font-family:Consolas,monospace}
|
||||
.footer{margin-top:18px;text-align:center;color:rgba(209,217,235,.68);font-size:.92rem}
|
||||
@media(max-width:1040px){
|
||||
@media(max-width:960px){
|
||||
body.mobile-drawer-open{overflow:hidden}
|
||||
.page-shell{width:min(100vw - 20px,1460px)}
|
||||
.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-mobile{display:block}
|
||||
.split,.grid--2,.grid--3,.grid--4{grid-template-columns:1fr}
|
||||
table{min-width:0}
|
||||
.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 rgba(163,183,255,.12);
|
||||
border-radius:18px;
|
||||
background:rgba(14,20,34,.96);
|
||||
box-shadow:0 16px 40px rgba(2,5,12,.34);
|
||||
}
|
||||
}
|
||||
@media(min-width:1041px){
|
||||
@media(min-width:961px){
|
||||
.site-mobile{display:none}
|
||||
}
|
||||
</style>
|
||||
@@ -277,32 +357,70 @@
|
||||
</a>
|
||||
|
||||
<nav class="site-nav" aria-label="Tenant-Menü">
|
||||
<?php foreach ($tenantNavItems as $item): ?>
|
||||
<?php foreach ($tenantHeaderNavItems 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; ?>
|
||||
<?php foreach ($tenantNavGroups as $group): ?>
|
||||
<details class="site-more">
|
||||
<summary class="site-more__toggle"><?= support_h((string) ($group['label'] ?? 'Mehr')) ?></summary>
|
||||
<div class="site-more__panel">
|
||||
<div class="site-more__label"><?= support_h((string) ($group['label'] ?? 'Mehr')) ?></div>
|
||||
<?php foreach (($group['items'] ?? []) 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; ?>
|
||||
</div>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
|
||||
<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">Menü</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>
|
||||
<button type="button" class="site-mobile__toggle" data-mobile-drawer-open>Mehr</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<?php if ($mobileBottomNavItems !== []): ?>
|
||||
<div class="mobile-drawer" data-mobile-drawer>
|
||||
<button type="button" class="mobile-drawer__scrim" aria-label="Menü schließen" data-mobile-drawer-close></button>
|
||||
<aside class="mobile-drawer__panel" aria-label="Mobile Navigation">
|
||||
<div class="mobile-drawer__header">
|
||||
<div>
|
||||
<h2 class="mobile-drawer__title">Mehr</h2>
|
||||
<p class="mobile-drawer__subtitle"><?= support_h((string) ($auth['tenant_name'] ?? 'Bereich')) ?></p>
|
||||
</div>
|
||||
<button type="button" class="mobile-drawer__close" aria-label="Schließen" data-mobile-drawer-close>×</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; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php foreach ($tenantNavGroups as $group): ?>
|
||||
<div class="mobile-drawer__section">
|
||||
<div class="mobile-drawer__label"><?= support_h((string) ($group['label'] ?? 'Mehr')) ?></div>
|
||||
<div class="mobile-drawer__links">
|
||||
<?php foreach (($group['items'] ?? []) 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>
|
||||
<?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>
|
||||
<form method="post" action="/logout/"><button type="submit" class="button button--ghost" style="width:100%">Abmelden</button></form>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="content">
|
||||
<section class="hero">
|
||||
<div>
|
||||
@@ -566,5 +684,86 @@
|
||||
<p class="footer">Die Kaffeeliste | Support, Hinweise und Betriebsprozesse im Tenant-Menü</p>
|
||||
</div>
|
||||
</main>
|
||||
<?php if ($mobileBottomNavItems !== []): ?>
|
||||
<nav class="mobile-bottom-nav" aria-label="Mobile Kernnavigation">
|
||||
<?php foreach ($mobileBottomNavItems as $item): ?>
|
||||
<a href="<?= support_h((string) ($item['href'] ?? '/')) ?>" class="mobile-bottom-nav__link <?= (($item['key'] ?? '') === 'support') ? 'active' : '' ?>" <?= (($item['key'] ?? '') === 'support') ? '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"><?= support_h((string) ($item['label'] ?? 'Link')) ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<button type="button" class="mobile-bottom-nav__toggle" data-mobile-drawer-open>
|
||||
<span class="mobile-bottom-nav__icon">+</span>
|
||||
<span class="mobile-bottom-nav__label">Mehr</span>
|
||||
</button>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const details = document.querySelectorAll('.site-more');
|
||||
const drawer = document.querySelector('[data-mobile-drawer]');
|
||||
const drawerOpeners = document.querySelectorAll('[data-mobile-drawer-open]');
|
||||
const drawerClosers = document.querySelectorAll('[data-mobile-drawer-close]');
|
||||
|
||||
function closeOthers(current) {
|
||||
details.forEach(function (other) {
|
||||
if (other !== current) {
|
||||
other.open = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
details.forEach(function (detail) {
|
||||
const summary = detail.querySelector('summary');
|
||||
if (summary) {
|
||||
summary.addEventListener('click', function () {
|
||||
if (!detail.open) {
|
||||
closeOthers(detail);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
detail.addEventListener('toggle', function () {
|
||||
if (detail.open) {
|
||||
closeOthers(detail);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.site-more__panel a, .mobile-drawer__link').forEach(function (link) {
|
||||
link.addEventListener('click', function () {
|
||||
details.forEach(function (detail) {
|
||||
detail.open = false;
|
||||
});
|
||||
if (drawer) {
|
||||
drawer.classList.remove('is-open');
|
||||
document.body.classList.remove('mobile-drawer-open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (drawer) {
|
||||
drawerOpeners.forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
drawer.classList.add('is-open');
|
||||
document.body.classList.add('mobile-drawer-open');
|
||||
});
|
||||
});
|
||||
|
||||
drawerClosers.forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
drawer.classList.remove('is-open');
|
||||
document.body.classList.remove('mobile-drawer-open');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -104,23 +104,28 @@
|
||||
}
|
||||
.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-mobile{display:none;position:relative}
|
||||
.site-mobile[open]{z-index:20}
|
||||
.site-toggle{
|
||||
.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::-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-mobile{display:none}
|
||||
.site-mobile__toggle{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap:12px;
|
||||
justify-content:center;
|
||||
gap:10px;
|
||||
cursor:pointer;
|
||||
list-style:none;
|
||||
padding:10px 12px;
|
||||
min-height:38px;
|
||||
padding:.45rem .72rem;
|
||||
border-radius:10px;
|
||||
border:1px solid rgba(201,214,255,.14);
|
||||
background:rgba(255,255,255,.04);
|
||||
color:#f4f7ff;
|
||||
font-weight:700;
|
||||
}
|
||||
.site-toggle::-webkit-details-marker{display:none}
|
||||
.site-toggle::before{
|
||||
.site-mobile__toggle::before{
|
||||
content:"";
|
||||
display:block;
|
||||
width:18px;
|
||||
@@ -129,26 +134,76 @@
|
||||
background:currentColor;
|
||||
box-shadow:0 -6px 0 currentColor,0 6px 0 currentColor;
|
||||
}
|
||||
.site-panel{
|
||||
.mobile-drawer{position:fixed;inset:0;display:none;z-index:55}
|
||||
.mobile-drawer.is-open{display:block}
|
||||
.mobile-drawer__scrim{position:absolute;inset:0;background:rgba(4,10,18,.58);backdrop-filter:blur(6px)}
|
||||
.mobile-drawer__panel{
|
||||
position:absolute;
|
||||
right:0;
|
||||
top:calc(100% + 12px);
|
||||
width:min(320px,calc(100vw - 32px));
|
||||
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);
|
||||
top:0;
|
||||
bottom:0;
|
||||
width:min(92vw,380px);
|
||||
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);
|
||||
display:grid;
|
||||
grid-template-rows:auto 1fr auto;
|
||||
gap:18px;
|
||||
overflow:auto;
|
||||
}
|
||||
.site-stack{display:grid;gap:10px}
|
||||
.site-stack .site-nav__link{
|
||||
justify-content:flex-start;
|
||||
padding:.6rem .75rem;
|
||||
.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__close{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
width:40px;
|
||||
height:40px;
|
||||
border-radius:999px;
|
||||
border:1px solid rgba(163,183,255,.12);
|
||||
background:rgba(255,255,255,.04);
|
||||
border-color:rgba(201,214,255,.1);
|
||||
color:#f4f7ff;
|
||||
font-size:1.15rem;
|
||||
font-weight:700;
|
||||
cursor:pointer;
|
||||
}
|
||||
.site-footer-actions{display:flex;justify-content:flex-end;margin-top:12px}
|
||||
.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__links{display:grid;gap:8px}
|
||||
.mobile-drawer__link{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
padding:.9rem 1rem;
|
||||
border-radius:14px;
|
||||
border:1px solid rgba(163,183,255,.12);
|
||||
background:rgba(255,255,255,.04);
|
||||
color:#f4f7ff;
|
||||
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__meta{display:grid;gap:4px}
|
||||
.mobile-bottom-nav{display:none}
|
||||
.mobile-bottom-nav__link,.mobile-bottom-nav__toggle{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
gap:6px;
|
||||
min-height:56px;
|
||||
padding:0 8px;
|
||||
border:0;
|
||||
background:transparent;
|
||||
color:rgba(231,238,255,.8);
|
||||
font:inherit;
|
||||
font-weight:700;
|
||||
}
|
||||
.mobile-bottom-nav__link.active,.mobile-bottom-nav__toggle.active{color:#f4f7ff}
|
||||
.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-radius:16px;
|
||||
@@ -223,11 +278,30 @@
|
||||
white-space:nowrap;
|
||||
}
|
||||
@media(max-width:960px){
|
||||
body.mobile-drawer-open{overflow:hidden}
|
||||
.page-shell{width:min(100vw - 20px,1460px)}
|
||||
.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-mobile{display:block}
|
||||
.hero,.grid--2,.grid--3,.grid--4{grid-template-columns:1fr}
|
||||
.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 rgba(163,183,255,.12);
|
||||
border-radius:18px;
|
||||
background:rgba(14,20,34,.96);
|
||||
box-shadow:0 16px 40px rgba(2,5,12,.34);
|
||||
}
|
||||
}
|
||||
@media(min-width:961px){
|
||||
.site-mobile{display:none}
|
||||
@@ -247,32 +321,70 @@
|
||||
</a>
|
||||
|
||||
<nav class="site-nav" aria-label="Tenant-Menü">
|
||||
<?php foreach ($tenantNavItems as $item): ?>
|
||||
<?php foreach ($tenantHeaderNavItems 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; ?>
|
||||
<?php foreach ($tenantNavGroups as $group): ?>
|
||||
<details class="site-more">
|
||||
<summary class="site-more__toggle"><?= tenant_roles_h((string) ($group['label'] ?? 'Mehr')) ?></summary>
|
||||
<div class="site-more__panel">
|
||||
<div class="site-more__label"><?= tenant_roles_h((string) ($group['label'] ?? 'Mehr')) ?></div>
|
||||
<?php foreach (($group['items'] ?? []) 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; ?>
|
||||
</div>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
|
||||
<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">Menü</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>
|
||||
<button type="button" class="site-mobile__toggle" data-mobile-drawer-open>Mehr</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<?php if ($mobileBottomNavItems !== []): ?>
|
||||
<div class="mobile-drawer" data-mobile-drawer>
|
||||
<button type="button" class="mobile-drawer__scrim" aria-label="Menü schließen" data-mobile-drawer-close></button>
|
||||
<aside class="mobile-drawer__panel" aria-label="Mobile Navigation">
|
||||
<div class="mobile-drawer__header">
|
||||
<div>
|
||||
<h2 class="mobile-drawer__title">Mehr</h2>
|
||||
<p class="mobile-drawer__subtitle"><?= tenant_roles_h((string) ($auth['tenant_name'] ?? 'Bereich')) ?></p>
|
||||
</div>
|
||||
<button type="button" class="mobile-drawer__close" aria-label="Schließen" data-mobile-drawer-close>×</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; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php foreach ($tenantNavGroups as $group): ?>
|
||||
<div class="mobile-drawer__section">
|
||||
<div class="mobile-drawer__label"><?= tenant_roles_h((string) ($group['label'] ?? 'Mehr')) ?></div>
|
||||
<div class="mobile-drawer__links">
|
||||
<?php foreach (($group['items'] ?? []) 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>
|
||||
<?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>
|
||||
<form method="post" action="/logout/"><button type="submit" class="button button--ghost" style="width:100%">Abmelden</button></form>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="content">
|
||||
<section class="hero">
|
||||
<div>
|
||||
@@ -426,5 +538,86 @@
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<?php if ($mobileBottomNavItems !== []): ?>
|
||||
<nav class="mobile-bottom-nav" aria-label="Mobile Kernnavigation">
|
||||
<?php foreach ($mobileBottomNavItems as $item): ?>
|
||||
<a class="mobile-bottom-nav__link <?= (($item['key'] ?? '') === 'roles') ? 'active' : '' ?>" href="<?= tenant_roles_h((string) ($item['href'] ?? '/')) ?>" <?= (($item['key'] ?? '') === 'roles') ? '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"><?= tenant_roles_h((string) ($item['label'] ?? 'Link')) ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<button type="button" class="mobile-bottom-nav__toggle" data-mobile-drawer-open>
|
||||
<span class="mobile-bottom-nav__icon">+</span>
|
||||
<span class="mobile-bottom-nav__label">Mehr</span>
|
||||
</button>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const details = document.querySelectorAll('.site-more');
|
||||
const drawer = document.querySelector('[data-mobile-drawer]');
|
||||
const drawerOpeners = document.querySelectorAll('[data-mobile-drawer-open]');
|
||||
const drawerClosers = document.querySelectorAll('[data-mobile-drawer-close]');
|
||||
|
||||
function closeOthers(current) {
|
||||
details.forEach(function (other) {
|
||||
if (other !== current) {
|
||||
other.open = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
details.forEach(function (detail) {
|
||||
const summary = detail.querySelector('summary');
|
||||
if (summary) {
|
||||
summary.addEventListener('click', function () {
|
||||
if (!detail.open) {
|
||||
closeOthers(detail);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
detail.addEventListener('toggle', function () {
|
||||
if (detail.open) {
|
||||
closeOthers(detail);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.site-more__panel a, .mobile-drawer__link').forEach(function (link) {
|
||||
link.addEventListener('click', function () {
|
||||
details.forEach(function (detail) {
|
||||
detail.open = false;
|
||||
});
|
||||
if (drawer) {
|
||||
drawer.classList.remove('is-open');
|
||||
document.body.classList.remove('mobile-drawer-open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (drawer) {
|
||||
drawerOpeners.forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
drawer.classList.add('is-open');
|
||||
document.body.classList.add('mobile-drawer-open');
|
||||
});
|
||||
});
|
||||
|
||||
drawerClosers.forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
drawer.classList.remove('is-open');
|
||||
document.body.classList.remove('mobile-drawer-open');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user