Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
732068ad9c | ||
|
|
ea434033f0 | ||
|
|
ebb71b4769 | ||
|
|
8c5668bf3d | ||
|
|
53baf0dfbf | ||
|
|
0c04caeb67 | ||
|
|
8b163c5734 | ||
|
|
a6007b5853 | ||
|
|
386b539f88 | ||
|
|
58c5912c30 | ||
|
|
17e07d525b | ||
|
|
dd019afff6 | ||
|
|
276a496ae8 | ||
|
|
9f10344fa7 | ||
|
|
c25d3f1b3c | ||
|
|
73e2144571 |
No files matched your search
@@ -25,14 +25,14 @@ Der Token wird nicht in dieser Dokumentation oder im Repository gespeichert.
|
||||
## Dokumentlinks in Marktdurchführungen
|
||||
|
||||
Wenn Hermes Informationen aus einer Datei verarbeitet und diese in einer
|
||||
Marktdurchführung speichert, muss Hermes die Quelldatei als vollständige
|
||||
HTTPS-URL in das Feld `notes` aufnehmen. Der bestehende Text darf erhalten
|
||||
bleiben; die Position im Freitext ist frei wählbar.
|
||||
Marktdurchführung speichert, muss Hermes die Quelldatei als Link im Feld
|
||||
`notes` aufnehmen. Der bestehende Text darf erhalten bleiben; die Position im
|
||||
Freitext ist frei wählbar.
|
||||
|
||||
Die URL verwendet dieses Format:
|
||||
Der Link verwendet dieses Format:
|
||||
|
||||
```text
|
||||
https://erpnaurua.imhochrain.ch/files.php?path=<RFC-3986-url-encodierter-relativer-Pfad>
|
||||
<a href="https://erpnaurua.imhochrain.ch/files.php?path=<RFC-3986-url-encodierter-relativer-Pfad>">Dokumentname</a>
|
||||
```
|
||||
|
||||
Der Parameter `path` ist relativ zum Ordner `Hermes Naurua` und enthält nicht
|
||||
@@ -41,12 +41,19 @@ Beispiel:
|
||||
|
||||
```text
|
||||
Quelldatei: Hermes Naurua/Marktmanagement/2026 Bellevue Noël/Standmietvertrag_Naurua.pdf
|
||||
Link: https://erpnaurua.imhochrain.ch/files.php?path=Marktmanagement%2F2026%20Bellevue%20No%C3%ABl%2FStandmietvertrag_Naurua.pdf
|
||||
Link: <a href="https://erpnaurua.imhochrain.ch/files.php?path=Marktmanagement%2F2026%20Bellevue%20No%C3%ABl%2FStandmietvertrag_Naurua.pdf">Standmietvertrag_Naurua.pdf</a>
|
||||
```
|
||||
|
||||
Leerzeichen, Umlaute und sonstige Sonderzeichen müssen vollständig URL-encodiert
|
||||
werden. Die ERP-Anzeige macht ausschließlich diese Dokument-URLs klickbar. Der
|
||||
Link ist schreibgeschützt und erfordert eine aktive ERP-Anmeldung.
|
||||
Leerzeichen, Umlaute und sonstige Sonderzeichen im URL-Parameter müssen
|
||||
vollständig URL-encodiert werden. Im Feld `notes` sind ausschließlich `<a>`
|
||||
für solche internen Dokumentlinks sowie `<br>` und `<strong>` erlaubt. Andere
|
||||
HTML-Tags und nicht erlaubte URL-Schemata werden entfernt. Externe `http://`-
|
||||
und `https://`-Links sind erlaubt und öffnen in einem neuen Tab. Der Link ist
|
||||
schreibgeschützt und erfordert bei internen Dokumenten eine aktive ERP-Anmeldung.
|
||||
Hermes muss diesen HTML-String unverändert als Feldwert senden: keine Markdown-
|
||||
Notation, keine Backticks und keine HTML-Entity-Kodierung wie `<`, `>` oder
|
||||
`"`. Der MCP-Transport erwartet den HTML-Inhalt im JSON-String und escaped
|
||||
ihn nicht als HTML.
|
||||
|
||||
Die Route benötigt auf dem ERP-Webserver die Konfiguration
|
||||
`HERMES_DOCUMENT_ROOT=/volume1/Hermes/Hermes Naurua`. Dieser lokale Pfad ist
|
||||
|
||||
@@ -69,9 +69,14 @@ Modulberechtigung.
|
||||
- Alle Schreibvorgänge laufen über die interne Modul-API.
|
||||
- Hermes darf bei einem neuen Markt nur eine bestehende Kategorie referenzieren.
|
||||
- Wenn Hermes Informationen aus einer Datei verarbeitet und in einer
|
||||
Marktdurchführung speichert, muss die Quelldatei als vollständige HTTPS-URL
|
||||
im Feld `notes` referenziert werden. Die Position im bestehenden Freitext ist
|
||||
frei wählbar.
|
||||
Marktdurchführung speichert, muss die Quelldatei als interner `<a>`-Link im
|
||||
Feld `notes` referenziert werden. Zusätzlich sind in mehrzeiligen Feldern
|
||||
`<br>` und `<strong>` erlaubt. Die Position im bestehenden Freitext ist frei
|
||||
wählbar. Externe `http://`- und `https://`-Links sind zulässig und öffnen in
|
||||
einem neuen Tab; andere HTML-Tags und URL-Schemata sind nicht zulässig. Hermes
|
||||
sendet den HTML-String unverändert; Markdown, Backticks und HTML-Entities wie
|
||||
`<`, `>` oder `"` werden nicht verwendet. Der MCP-Transport escaped
|
||||
den HTML-Inhalt nicht.
|
||||
- Unsichere Erkennung wird nicht durch eine zusätzliche App-Freigabelogik
|
||||
behandelt; die fachliche Qualitätssicherung liegt im Hermes-Training.
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Mobile Detail
|
||||
|
||||
## Zweck
|
||||
|
||||
`SharedMobileDetail` ist ein fachlich neutrales Frontend-Pattern fuer mobile
|
||||
Listen-/Detailansichten. Es ersetzt auf kleinen Viewports den sichtbaren
|
||||
Drawer durch eine vollflaechige Detailansicht.
|
||||
|
||||
## Verantwortung
|
||||
|
||||
Das Pattern kapselt ausschliesslich technische Interaktion:
|
||||
|
||||
- Verschieben des vorhandenen Detail-DOMs in einen mobilen Host
|
||||
- Anzeigen und Entfernen des mobilen Hosts
|
||||
- Browser-History und `popstate`-kompatibler Zurueck-Navigation
|
||||
- Zuruecksetzen des Body-Locks und des globalen Drawer-Zustands
|
||||
- Zuruecksetzen der Drawer-Attribute beim Verlassen der Detailansicht
|
||||
|
||||
Fachliche Darstellung, API-Aufrufe, Validierung und Speichern bleiben im
|
||||
owning Modul.
|
||||
|
||||
## Nutzung
|
||||
|
||||
Fachliche Bereiche erzeugen einen Controller mit eigenem Host-/Drawer-
|
||||
Attribut, eigener Body-Klasse und einem bestehenden Styleguide-
|
||||
Navigationskarten-Markup fuer die Zurueck-Navigation. Der Controller darf
|
||||
keine fachlichen Daten oder fachlichen Zustandsentscheidungen enthalten.
|
||||
@@ -1 +1 @@
|
||||
2026.05.18.1
|
||||
2026.05.18.2
|
||||
@@ -1,41 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Styleguide – Komponente E-Mail Footer</title>
|
||||
<link rel="stylesheet" href="../styleguide.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1 class="sg-main-heading">Komponente – E-Mail Footer</h1>
|
||||
|
||||
<section id="component-email-footer">
|
||||
<p class="sg-preview-label">Komponente: E-Mail Footer</p>
|
||||
|
||||
<p class="sg-preview-label">E-Mail Footer not subscribed</p>
|
||||
|
||||
<div class="sg-preview-area">
|
||||
<article
|
||||
class="sg-card sg-card--content-card sg-card--footer-card"
|
||||
data-component="content-card"
|
||||
data-component-context="email-footer"
|
||||
data-component-variant="not-subscribed"
|
||||
aria-label="E-Mail Footer not subscribed"
|
||||
>
|
||||
<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body">
|
||||
<div class="sg-text-layout-pattern__sample sg-text-layout-pattern__two-column" data-pattern-part="text-block-two-column">
|
||||
<p class="sg-body sg-text-layout-pattern__column">
|
||||
Footer links: Platzhaltertext fuer allgemeine Hinweise, Navigation oder Kontaktinformationen im zweispaltigen Layout.
|
||||
</p>
|
||||
<p class="sg-body sg-text-layout-pattern__column">
|
||||
Footer rechts: Platzhaltertext fuer ergaenzende Angaben, rechtliche Hinweise oder sekundäre Footer-Inhalte im gleichen Raster.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,37 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Styleguide – Komponente E-Mail Header</title>
|
||||
<link rel="stylesheet" href="../styleguide.css">
|
||||
<link rel="stylesheet" href="../email-styleguide.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1 class="sg-main-heading">Komponente – E-Mail Header</h1>
|
||||
|
||||
<section id="component-email-header">
|
||||
<p class="sg-preview-label">Komponente: E-Mail Header</p>
|
||||
|
||||
<div class="sg-preview-area">
|
||||
<div class="sg-email-preview-viewport">
|
||||
<div class="sg-email-preview-body">
|
||||
<header
|
||||
class="sg-email-header"
|
||||
aria-label="E-Mail Header"
|
||||
data-component="email-header"
|
||||
data-component-variant="logo-only"
|
||||
data-derived-from="portal-header-without-options-row"
|
||||
>
|
||||
<div class="sg-email-header__main" data-component-part="main">
|
||||
<p class="sg-email-header__brand" data-component-part="brand">ValueStockFinder</p>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -31,6 +31,7 @@
|
||||
<tr><td>white</td><td>#FFFFFF</td><td>Standardfläche von Buttons, Eingabefeldern, Pulldown-Triggern, Checkboxen, Radios, Toggle-Tracks und Card-Trennern.</td></tr>
|
||||
<tr><td>signal-green</td><td>#009101</td><td>Positive Score-Balken und positive Chart-Werte.</td></tr>
|
||||
<tr><td>signal-green-light</td><td>#65A665</td><td>Hellgrüne Variante für positive Signal- und Notification-Flächen.</td></tr>
|
||||
<tr><td>signal-green-red</td><td>#C0624D</td><td>Rotgrüne Variante für positive oder negative Bewertungsdarstellungen in Notifications.</td></tr>
|
||||
<tr><td>signal-yellow</td><td>#9C7A00</td><td>Neutrale Score-Balken, neutrale Chart-Werte und Warnwerte in Tabellen.</td></tr>
|
||||
<tr><td>ampel-hellgelb</td><td>#D3B962</td><td>Untersegmente in der Pattern-Notification-Variante der Object Card.</td></tr>
|
||||
<tr><td>signal-red</td><td>#9B3B2F</td><td>Negative Score-Balken und negative Chart-Werte.</td></tr>
|
||||
@@ -38,13 +39,29 @@
|
||||
<tr><td>font-light</td><td>#FFFFFF</td><td>Schrift auf dunklen Flächen, Portalheader, Card-Headern, Prozessbuttons und ausgewählten Tab-Buttons.</td></tr>
|
||||
<tr><td>font-hyperlink</td><td>#FF6900</td><td>Links in Texten, Prozessbuttons und Page-Navigations-Cards.</td></tr>
|
||||
<tr><td>process-inactive</td><td>#FFAE79</td><td>Inaktive Hintergrundfläche des Prozessbuttons.</td></tr>
|
||||
<tr><td>background-purple</td><td>#373C4A</td><td>Globaler Portal- und E-Mail-Hintergrund.</td></tr>
|
||||
<tr><td>background-purple</td><td>#373C4A</td><td>Globaler Portalhintergrund.</td></tr>
|
||||
<tr><td>background-purple-light</td><td>#656C7D</td><td>Group Cards und gruppierende Card-Flächen.</td></tr>
|
||||
<tr><td>transparent</td><td>transparent</td><td>Transparenter Foundation-Wert für semantische Overlay-/Box-Varianten ohne Eigenfläche.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="foundations-card-text">
|
||||
<h2 class="sg-sub-heading sg-text-on-dark">Card Semantic Tokens</h2>
|
||||
<table class="sg-foundation-table sg-table-label">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Wert</th>
|
||||
<th>Verwendung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>text-card-signal</td><td>font-light</td><td>Weiße Schrift auf allen farbigen Signal-Segmenten, unabhängig vom übergeordneten Card-Kontext.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="foundations-states">
|
||||
<h2 class="sg-sub-heading sg-text-on-dark">States</h2>
|
||||
<table class="sg-foundation-table sg-table-label">
|
||||
@@ -156,7 +173,6 @@
|
||||
<tr><td>input-label-width</td><td>9rem</td><td>Desktop-Breite der Label-Spalte für gruppierte Formularzeilen mit Pulldowns, Slidern, Radio-Feldern, Checkbox-Feldern sowie ein- und mehrzeiligen Eingabefeldern.</td></tr>
|
||||
<tr><td>input-field-desktop-width</td><td>400px</td><td>Fixe Desktop-Breite von ein- und mehrzeiligen Eingabefeldern in der Input-Component-Preview.</td></tr>
|
||||
<tr><td>input-field-max-width</td><td>600px</td><td>Maximale Breite von ein- und mehrzeiligen Eingabefeldern (ohne Suchfeld).</td></tr>
|
||||
<tr><td>email-body-max-width</td><td>600px</td><td>Maximale Breite des Inhaltsbereichs von E-Mail-Templates innerhalb des E-Mail-Viewports.</td></tr>
|
||||
<tr><td>object-card-height</td><td>600px</td><td>Fixe Höhe der Object Card im Desktop-Layout (u. a. für Company Card).</td></tr>
|
||||
<tr><td>scrollable-card-width</td><td>600px</td><td>Standardbreite der eigenständigen Scrollable Card; kann im konsumierenden Portal über den semantischen Layout-Token angepasst werden.</td></tr>
|
||||
<tr><td>scrollable-card-height</td><td>600px</td><td>Standardhöhe der eigenständigen Scrollable Card; kann im konsumierenden Portal über den semantischen Layout-Token angepasst werden.</td></tr>
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<li><a href="./patterns/navigation-card.html">Navigation Card</a></li>
|
||||
<li><a href="./patterns/left-navigation.html">Left Navigation</a></li>
|
||||
<li><a href="./patterns/notifications.html">Notifications</a></li>
|
||||
<li><a href="./patterns/history-graph.html">History Graph</a></li>
|
||||
<li><a href="./patterns/content-cards-group.html">Content Cards Group</a></li>
|
||||
<li><a href="./patterns/card-gruppe-mit-tastennavigation.html">Card Gruppe mit Tastennavigation</a></li>
|
||||
<li><a href="./patterns/formular-mit-abschnitten.html">Formular mit Abschnitten</a></li>
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Styleguide – Pattern History Graph</title>
|
||||
<link rel="stylesheet" href="../styleguide.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1 class="sg-main-heading">Pattern – History Graph</h1>
|
||||
|
||||
<section id="pattern-history-graph">
|
||||
<p class="sg-preview-label">Pattern: History Graph</p>
|
||||
|
||||
<div class="sg-preview-area sg-history-graph" aria-label="Verlauf von Ereignissen">
|
||||
<ol class="sg-history-graph__events">
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Aktuellstes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green" data-component-part="card-header">
|
||||
<p class="sg-body">Aktuellstes Ereignis mit einem einzelnen farbigen Segment.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Zweites Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-yellow" data-component-part="card-header">
|
||||
<p class="sg-body">Ereignis mit mehreren unabhängig gefärbten Segmenten.</p>
|
||||
</div>
|
||||
<div class="sg-card-segment sg-card-segment--body sg-card-segment--white" data-component-part="card-body">
|
||||
<p class="sg-body">Der Inhalt, zum Beispiel Datum und Beschreibung, wird individuell durch das konsumierende Portal definiert.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Ältestes dargestelltes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-red" data-component-part="card-header">
|
||||
<p class="sg-body">Ältestes dargestelltes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Viertes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green-light" data-component-part="card-header">
|
||||
<p class="sg-body">Viertes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Fünftes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-yellow" data-component-part="card-header">
|
||||
<p class="sg-body">Fünftes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Sechstes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green" data-component-part="card-header">
|
||||
<p class="sg-body">Sechstes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Siebtes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-red" data-component-part="card-header">
|
||||
<p class="sg-body">Siebtes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Achtes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green-light" data-component-part="card-header">
|
||||
<p class="sg-body">Achtes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Neuntes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-yellow" data-component-part="card-header">
|
||||
<p class="sg-body">Neuntes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Zehntes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green" data-component-part="card-header">
|
||||
<p class="sg-body">Zehntes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<!-- Wird nur gerendert, wenn weitere Ereignisse geladen werden können. -->
|
||||
<div class="sg-history-graph__navigation">
|
||||
<div class="sg-navigation-card-layout">
|
||||
<div class="sg-navigation-card-block">
|
||||
<article class="sg-card" data-component="card" data-pattern="navigation-card" aria-label="Weitere Ereignisse laden">
|
||||
<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment">
|
||||
<div class="sg-navigation-card-center">
|
||||
<a class="sg-hyperlink" href="#" data-component="hyperlink">Mehr laden</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -142,6 +142,27 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<p class="sg-preview-label">Pattern: Notification with title - green-red rating</p>
|
||||
<article class="sg-card sg-card--notification-signal-green-red sg-notifications-pattern__card" data-pattern="notification-with-title" data-pattern-variant="green-red-rating" data-component="notification-card">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green-red sg-notifications-pattern__text-segment sg-notifications-pattern__title-segment" data-component-part="card-header">
|
||||
<p class="sg-body">
|
||||
<span class="sg-strong">Bewertung</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green-red sg-notifications-pattern__text-segment" data-component-part="card-header">
|
||||
<p class="sg-body">
|
||||
Positive oder negative Bewertungsdarstellung zur Illustration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="sg-card-segment sg-card-segment--body sg-card-segment--signal-green-red" data-component-part="card-body">
|
||||
<button class="sg-interaction-element sg-button sg-button--active" type="button" data-component="button" data-component-state="active">
|
||||
zur Bewertung
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<p class="sg-preview-label">Pattern: Notification with title - yellow</p>
|
||||
<article class="sg-card sg-notifications-pattern__card" data-pattern="notification-with-title" data-pattern-variant="yellow" data-component="notification-card">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-yellow sg-notifications-pattern__text-segment sg-notifications-pattern__title-segment" data-component-part="card-header">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</p>
|
||||
|
||||
<p class="sg-table-label" style="color: var(--text-options-row-description);">
|
||||
Mobile: rechte Optionen werden zum oberen Segment, linke Optionen zum unteren Segment; linke Optionen brechen innerhalb der verfügbaren Breite um.
|
||||
Mobile: Pulldowns stehen mit maximal zwei Elementen pro Zeile; ein Suchfeld steht immer allein in einer 100-%-Zeile.
|
||||
</p>
|
||||
|
||||
<div class="sg-options-row" aria-label="Optionszeile" data-pattern="options-row">
|
||||
@@ -83,6 +83,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sg-pulldown-demo" data-open="false" data-align="left" data-selection-mode="single" data-component="pulldown" data-component-context="options-row" data-component-state="inactive-selectable">
|
||||
<button class="sg-interaction-element sg-pulldown sg-pulldown-demo__trigger" type="button" aria-expanded="false" aria-label="Very long English label for market selection" data-component-part="pulldown-trigger" data-label-base="Very long English market selection label">
|
||||
Very long English market selection label
|
||||
</button>
|
||||
|
||||
<div class="sg-pulldown-panel" aria-label="Opened market selection" data-component-part="pulldown-panel">
|
||||
<ul class="sg-pulldown-option-list" aria-label="Market options">
|
||||
<li class="sg-pulldown-option" role="checkbox" aria-checked="false" data-pulldown-option>
|
||||
<span>United Kingdom</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sg-search-field-row">
|
||||
<span class="sg-input-single-line-wrap sg-search-field-input" data-has-value="false" data-component="single-line-input" data-component-context="options-row" data-component-state="inactive-selectable">
|
||||
<input
|
||||
@@ -120,6 +134,39 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="pattern-options-row-mobile-cases">
|
||||
<p class="sg-preview-label">Pattern: Options Row – Mobile-Fälle</p>
|
||||
<p class="sg-table-label" style="color: var(--text-options-row-description);">
|
||||
Prüffälle: kein Pulldown, ein Pulldown, Suchfeld ohne Ergebniszähler sowie lange deutsche Beschriftung.
|
||||
</p>
|
||||
|
||||
<div class="sg-options-row sg-options-row--left-only" aria-label="Optionszeile ohne Pulldown" data-pattern="options-row-mobile-no-pulldown">
|
||||
<div class="sg-options-row__left" data-pattern-part="options-row-primary-actions">
|
||||
<div class="sg-search-field-row">
|
||||
<span class="sg-input-single-line-wrap sg-search-field-input" data-has-value="false" data-component="single-line-input" data-component-context="options-row" data-component-state="inactive-selectable">
|
||||
<input class="sg-interaction-element sg-input-single-line" type="text" placeholder="Suche ohne Ergebniszähler" aria-label="Suche ohne Ergebniszähler">
|
||||
<button class="sg-input-clear-button" type="button" aria-label="Eingabe löschen">×</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sg-options-row sg-options-row--left-only" aria-label="Optionszeile mit einem Pulldown" data-pattern="options-row-mobile-one-pulldown">
|
||||
<div class="sg-options-row__left" data-pattern-part="options-row-primary-actions">
|
||||
<div class="sg-pulldown-demo" data-open="false" data-align="left" data-selection-mode="single" data-component="pulldown" data-component-context="options-row" data-component-state="inactive-selectable">
|
||||
<button class="sg-interaction-element sg-pulldown sg-pulldown-demo__trigger" type="button" aria-expanded="false" aria-label="Sehr lange deutsche Beschriftung für eine Auswahl" data-component-part="pulldown-trigger" data-label-base="Sehr lange deutsche Beschriftung für eine Auswahl">
|
||||
Sehr lange deutsche Beschriftung für eine Auswahl
|
||||
</button>
|
||||
<div class="sg-pulldown-panel" aria-label="Geöffnete Auswahl" data-component-part="pulldown-panel">
|
||||
<ul class="sg-pulldown-option-list" aria-label="Auswahloptionen">
|
||||
<li class="sg-pulldown-option" role="checkbox" aria-checked="false" data-pulldown-option><span>Option</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="pattern-options-row-left-only">
|
||||
<p class="sg-preview-label">Pattern: Options row left only</p>
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
(function initTooltipOverlayModule() {
|
||||
const getViewportWidth = () => {
|
||||
if (window.visualViewport && typeof window.visualViewport.width === 'number') {
|
||||
return window.visualViewport.width;
|
||||
}
|
||||
return window.innerWidth;
|
||||
};
|
||||
let tooltipId = 0;
|
||||
const states = new WeakMap();
|
||||
const tooltipStates = new Set();
|
||||
|
||||
const getViewport = () => ({
|
||||
width: window.visualViewport?.width || window.innerWidth,
|
||||
height: window.visualViewport?.height || window.innerHeight,
|
||||
});
|
||||
|
||||
const getPixelValue = (propertyName) => {
|
||||
const rootStyles = getComputedStyle(document.documentElement);
|
||||
@@ -21,144 +23,343 @@
|
||||
return value;
|
||||
};
|
||||
|
||||
const setTooltipState = (wrap, isOpen) => {
|
||||
const trigger = wrap.querySelector('[data-component-part="tooltip-trigger"]');
|
||||
const panel = wrap.querySelector('[data-component-part="tooltip-panel"]');
|
||||
if (!trigger || !panel) {
|
||||
return;
|
||||
const getElements = (wrap) => ({
|
||||
trigger: wrap.querySelector('[data-component-part="tooltip-trigger"]'),
|
||||
panel: wrap.querySelector('[data-component-part="tooltip-panel"]'),
|
||||
});
|
||||
|
||||
const ensureAccessibility = (state) => {
|
||||
const { trigger, panel } = state;
|
||||
panel.setAttribute('role', 'tooltip');
|
||||
|
||||
if (!panel.id) {
|
||||
tooltipId += 1;
|
||||
panel.id = `sg-tooltip-panel-${tooltipId}`;
|
||||
}
|
||||
|
||||
const describedBy = (trigger.getAttribute('aria-describedby') || '')
|
||||
.split(/\s+/)
|
||||
.filter(Boolean);
|
||||
if (!describedBy.includes(panel.id)) {
|
||||
trigger.setAttribute('aria-describedby', [...describedBy, panel.id].join(' '));
|
||||
}
|
||||
|
||||
const hasAccessibleName = Boolean(
|
||||
trigger.getAttribute('aria-label')
|
||||
|| trigger.getAttribute('aria-labelledby')
|
||||
|| trigger.getAttribute('title')
|
||||
|| trigger.textContent.trim(),
|
||||
);
|
||||
if (!hasAccessibleName && panel.textContent.trim()) {
|
||||
trigger.setAttribute('aria-label', panel.textContent.trim());
|
||||
}
|
||||
|
||||
if (!trigger.matches('a,button,input,select,textarea,[tabindex]')) {
|
||||
trigger.setAttribute('tabindex', '0');
|
||||
}
|
||||
};
|
||||
|
||||
const setOpenState = (state, isOpen) => {
|
||||
const { wrap, panel, trigger } = state;
|
||||
wrap.dataset.tooltipOpen = String(isOpen);
|
||||
panel.hidden = !isOpen;
|
||||
trigger.setAttribute('aria-expanded', String(isOpen));
|
||||
|
||||
if (trigger.matches('button,[role="button"],[aria-expanded]')) {
|
||||
trigger.setAttribute('aria-expanded', String(isOpen));
|
||||
}
|
||||
};
|
||||
|
||||
const positionTooltip = (wrap) => {
|
||||
const trigger = wrap.querySelector('[data-component-part="tooltip-trigger"]');
|
||||
const panel = wrap.querySelector('[data-component-part="tooltip-panel"]');
|
||||
if (!trigger || !panel || panel.hidden) {
|
||||
const restorePanel = (state) => {
|
||||
const { panel, placeholder } = state;
|
||||
panel.hidden = true;
|
||||
panel.removeAttribute('data-tooltip-portal');
|
||||
panel.removeAttribute('data-tooltip-placement');
|
||||
panel.style.removeProperty('left');
|
||||
panel.style.removeProperty('top');
|
||||
panel.style.removeProperty('visibility');
|
||||
panel.style.removeProperty('--tooltip-arrow-offset');
|
||||
|
||||
if (placeholder.parentNode) {
|
||||
placeholder.parentNode.insertBefore(panel, placeholder.nextSibling);
|
||||
}
|
||||
};
|
||||
|
||||
const movePanelToPortal = (state) => {
|
||||
const { panel } = state;
|
||||
if (panel.parentNode !== document.body) {
|
||||
document.body.appendChild(panel);
|
||||
}
|
||||
panel.setAttribute('data-tooltip-portal', 'true');
|
||||
panel.hidden = false;
|
||||
panel.style.visibility = 'hidden';
|
||||
};
|
||||
|
||||
const setArrowOffset = (state, placement, panelLeft, panelTop, panelWidth, panelHeight) => {
|
||||
const { trigger, panel } = state;
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const offset = placement === 'left' || placement === 'right'
|
||||
? triggerRect.top + (triggerRect.height / 2) - panelTop
|
||||
: triggerRect.left + (triggerRect.width / 2) - panelLeft;
|
||||
const maxOffset = placement === 'left' || placement === 'right' ? panelHeight : panelWidth;
|
||||
panel.style.setProperty('--tooltip-arrow-offset', `${Math.max(0, Math.min(offset, maxOffset))}px`);
|
||||
};
|
||||
|
||||
const positionTooltip = (state) => {
|
||||
const { trigger, panel } = state;
|
||||
if (panel.hidden || panel.parentNode !== document.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportWidth = getViewportWidth();
|
||||
const viewport = getViewport();
|
||||
const safeInset = getPixelValue('--spacing-large');
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const gap = getPixelValue('--spacing-small');
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const panelWidth = panel.offsetWidth;
|
||||
const panelHeight = panel.offsetHeight;
|
||||
const centeredLeft = triggerRect.left + (triggerRect.width / 2) - (panelWidth / 2);
|
||||
const centeredRight = centeredLeft + panelWidth;
|
||||
const hasBottomSpace = triggerRect.bottom + gap + panelHeight <= viewport.height - safeInset;
|
||||
const hasTopSpace = triggerRect.top - gap - panelHeight >= safeInset;
|
||||
const hasLeftSpace = triggerRect.left - gap - panelWidth >= safeInset;
|
||||
const hasRightSpace = triggerRect.right + gap + panelWidth <= viewport.width - safeInset;
|
||||
|
||||
let placement = 'bottom';
|
||||
if (triggerRect.right + (panelRect.width / 2) > viewportWidth - safeInset) {
|
||||
if (centeredRight > viewport.width - safeInset && hasLeftSpace) {
|
||||
placement = 'left';
|
||||
} else if (triggerRect.left - (panelRect.width / 2) < safeInset) {
|
||||
} else if (centeredLeft < safeInset && hasRightSpace) {
|
||||
placement = 'right';
|
||||
} else if (!hasBottomSpace && hasTopSpace) {
|
||||
placement = 'top';
|
||||
}
|
||||
|
||||
wrap.dataset.tooltipPlacement = placement;
|
||||
let panelLeft = centeredLeft;
|
||||
let panelTop = triggerRect.bottom + gap;
|
||||
|
||||
if (placement === 'bottom') {
|
||||
const nextPanelRect = panel.getBoundingClientRect();
|
||||
if (nextPanelRect.bottom > window.innerHeight - safeInset
|
||||
&& triggerRect.top - panelRect.height - gap >= safeInset) {
|
||||
wrap.dataset.tooltipPlacement = 'top';
|
||||
if (placement === 'top') {
|
||||
panelTop = triggerRect.top - gap - panelHeight;
|
||||
} else if (placement === 'left') {
|
||||
panelLeft = triggerRect.left - gap - panelWidth;
|
||||
panelTop = triggerRect.top + (triggerRect.height / 2) - (panelHeight / 2);
|
||||
} else if (placement === 'right') {
|
||||
panelLeft = triggerRect.right + gap;
|
||||
panelTop = triggerRect.top + (triggerRect.height / 2) - (panelHeight / 2);
|
||||
}
|
||||
|
||||
panelLeft = Math.max(safeInset, Math.min(panelLeft, viewport.width - safeInset - panelWidth));
|
||||
panelTop = Math.max(safeInset, Math.min(panelTop, viewport.height - safeInset - panelHeight));
|
||||
|
||||
panel.dataset.tooltipPlacement = placement;
|
||||
state.wrap.dataset.tooltipPlacement = placement;
|
||||
panel.style.left = `${Math.round(panelLeft)}px`;
|
||||
panel.style.top = `${Math.round(panelTop)}px`;
|
||||
setArrowOffset(state, placement, panelLeft, panelTop, panelWidth, panelHeight);
|
||||
panel.style.visibility = 'visible';
|
||||
};
|
||||
|
||||
const openTooltip = (state) => {
|
||||
tooltipStates.forEach((otherState) => {
|
||||
if (otherState !== state) {
|
||||
closeTooltip(otherState);
|
||||
}
|
||||
});
|
||||
movePanelToPortal(state);
|
||||
setOpenState(state, true);
|
||||
positionTooltip(state);
|
||||
};
|
||||
|
||||
const closeTooltip = (state) => {
|
||||
state.pinned = false;
|
||||
state.hoverTrigger = false;
|
||||
state.hoverPanel = false;
|
||||
state.focused = false;
|
||||
state.dismissed = false;
|
||||
setOpenState(state, false);
|
||||
restorePanel(state);
|
||||
};
|
||||
|
||||
const closeIfInactive = (state) => {
|
||||
if (!state.pinned && !state.hoverTrigger && !state.hoverPanel && !state.focused) {
|
||||
setOpenState(state, false);
|
||||
restorePanel(state);
|
||||
}
|
||||
};
|
||||
|
||||
const closeTooltip = (wrap) => {
|
||||
wrap.dataset.tooltipPinned = 'false';
|
||||
wrap.dataset.tooltipHover = 'false';
|
||||
wrap.dataset.tooltipFocus = 'false';
|
||||
wrap.dataset.tooltipDismissed = 'false';
|
||||
setTooltipState(wrap, false);
|
||||
};
|
||||
const togglePinned = (state) => {
|
||||
if (state.pinned) {
|
||||
state.pinned = false;
|
||||
state.dismissed = true;
|
||||
setOpenState(state, false);
|
||||
restorePanel(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const openTooltip = (wrap) => {
|
||||
setTooltipState(wrap, true);
|
||||
positionTooltip(wrap);
|
||||
state.pinned = true;
|
||||
state.dismissed = false;
|
||||
openTooltip(state);
|
||||
};
|
||||
|
||||
const initTooltip = (wrap) => {
|
||||
if (wrap.dataset.tooltipInit === 'true') {
|
||||
if (states.has(wrap)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = getElements(wrap);
|
||||
if (!elements.trigger || !elements.panel || !elements.panel.parentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const placeholder = document.createComment('sg-tooltip-panel-placeholder');
|
||||
elements.panel.parentNode.insertBefore(placeholder, elements.panel);
|
||||
|
||||
const state = {
|
||||
wrap,
|
||||
trigger: elements.trigger,
|
||||
panel: elements.panel,
|
||||
placeholder,
|
||||
pinned: false,
|
||||
hoverTrigger: false,
|
||||
hoverPanel: false,
|
||||
focused: false,
|
||||
dismissed: false,
|
||||
};
|
||||
wrap.dataset.tooltipInit = 'true';
|
||||
wrap.dataset.tooltipPlacement = 'bottom';
|
||||
wrap.dataset.tooltipPinned = 'false';
|
||||
wrap.dataset.tooltipHover = 'false';
|
||||
wrap.dataset.tooltipFocus = 'false';
|
||||
wrap.dataset.tooltipDismissed = 'false';
|
||||
setTooltipState(wrap, false);
|
||||
states.set(wrap, state);
|
||||
tooltipStates.add(state);
|
||||
ensureAccessibility(state);
|
||||
setOpenState(state, false);
|
||||
|
||||
const trigger = wrap.querySelector('[data-component-part="tooltip-trigger"]');
|
||||
if (!trigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
wrap.addEventListener('pointerenter', () => {
|
||||
if (wrap.dataset.tooltipDismissed === 'true') {
|
||||
wrap.addEventListener('pointerenter', (event) => {
|
||||
if (event.pointerType !== 'mouse') {
|
||||
return;
|
||||
}
|
||||
wrap.dataset.tooltipHover = 'true';
|
||||
openTooltip(wrap);
|
||||
if (state.dismissed) {
|
||||
return;
|
||||
}
|
||||
state.hoverTrigger = true;
|
||||
openTooltip(state);
|
||||
});
|
||||
|
||||
wrap.addEventListener('pointerleave', () => {
|
||||
wrap.dataset.tooltipHover = 'false';
|
||||
wrap.dataset.tooltipDismissed = 'false';
|
||||
if (wrap.dataset.tooltipPinned !== 'true' && wrap.dataset.tooltipFocus !== 'true') {
|
||||
setTooltipState(wrap, false);
|
||||
wrap.addEventListener('pointerleave', (event) => {
|
||||
if (event.pointerType !== 'mouse') {
|
||||
return;
|
||||
}
|
||||
state.hoverTrigger = false;
|
||||
state.dismissed = false;
|
||||
if (!state.panel.contains(event.relatedTarget)) {
|
||||
closeIfInactive(state);
|
||||
}
|
||||
});
|
||||
|
||||
state.panel.addEventListener('pointerenter', (event) => {
|
||||
if (event.pointerType !== 'mouse') {
|
||||
return;
|
||||
}
|
||||
if (!state.dismissed) {
|
||||
state.hoverPanel = true;
|
||||
openTooltip(state);
|
||||
}
|
||||
});
|
||||
|
||||
state.panel.addEventListener('pointerleave', (event) => {
|
||||
if (event.pointerType !== 'mouse') {
|
||||
return;
|
||||
}
|
||||
state.hoverPanel = false;
|
||||
state.dismissed = false;
|
||||
closeIfInactive(state);
|
||||
});
|
||||
|
||||
wrap.addEventListener('focusin', () => {
|
||||
if (wrap.dataset.tooltipDismissed === 'true') {
|
||||
return;
|
||||
if (!state.dismissed) {
|
||||
state.focused = true;
|
||||
openTooltip(state);
|
||||
}
|
||||
wrap.dataset.tooltipFocus = 'true';
|
||||
openTooltip(wrap);
|
||||
});
|
||||
|
||||
wrap.addEventListener('focusout', (event) => {
|
||||
if (wrap.contains(event.relatedTarget)) {
|
||||
if (wrap.contains(event.relatedTarget) || state.panel.contains(event.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
wrap.dataset.tooltipFocus = 'false';
|
||||
wrap.dataset.tooltipDismissed = 'false';
|
||||
if (wrap.dataset.tooltipPinned !== 'true' && wrap.dataset.tooltipHover !== 'true') {
|
||||
setTooltipState(wrap, false);
|
||||
}
|
||||
state.focused = false;
|
||||
state.dismissed = false;
|
||||
closeIfInactive(state);
|
||||
});
|
||||
|
||||
trigger.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const isPinned = wrap.dataset.tooltipPinned === 'true';
|
||||
wrap.dataset.tooltipPinned = String(!isPinned);
|
||||
state.trigger.addEventListener('click', (event) => {
|
||||
togglePinned(state);
|
||||
});
|
||||
|
||||
if (isPinned) {
|
||||
wrap.dataset.tooltipDismissed = 'true';
|
||||
setTooltipState(wrap, false);
|
||||
return;
|
||||
state.trigger.addEventListener('keydown', (event) => {
|
||||
const isNativeInteractive = state.trigger.matches('a,button,input,select,textarea');
|
||||
if (!isNativeInteractive && (event.key === 'Enter' || event.key === ' ')) {
|
||||
event.preventDefault();
|
||||
togglePinned(state);
|
||||
}
|
||||
|
||||
wrap.dataset.tooltipDismissed = 'false';
|
||||
openTooltip(wrap);
|
||||
});
|
||||
};
|
||||
|
||||
document.querySelectorAll('.sg-tooltip-wrap').forEach(initTooltip);
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
document.querySelectorAll('.sg-tooltip-wrap').forEach((wrap) => {
|
||||
if (!wrap.contains(event.target)) {
|
||||
closeTooltip(wrap);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const repositionOpenTooltips = () => {
|
||||
document.querySelectorAll('.sg-tooltip-wrap[data-tooltip-open="true"]').forEach(positionTooltip);
|
||||
const initializeWithin = (root) => {
|
||||
if (root.nodeType === Node.DOCUMENT_NODE) {
|
||||
root.querySelectorAll('.sg-tooltip-wrap').forEach(initTooltip);
|
||||
return;
|
||||
}
|
||||
if (root.nodeType !== Node.ELEMENT_NODE) {
|
||||
return;
|
||||
}
|
||||
const owningWrap = root.closest('.sg-tooltip-wrap');
|
||||
if (owningWrap) {
|
||||
initTooltip(owningWrap);
|
||||
}
|
||||
if (root.matches('.sg-tooltip-wrap')) {
|
||||
initTooltip(root);
|
||||
}
|
||||
root.querySelectorAll('.sg-tooltip-wrap').forEach(initTooltip);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', repositionOpenTooltips);
|
||||
window.addEventListener('scroll', repositionOpenTooltips, true);
|
||||
const bootstrap = () => {
|
||||
window.sgInitTooltips = (options = {}) => {
|
||||
const root = options.root || document.body;
|
||||
initializeWithin(root);
|
||||
};
|
||||
|
||||
window.sgInitTooltips();
|
||||
|
||||
const observer = new MutationObserver((records) => {
|
||||
records.forEach((record) => {
|
||||
record.addedNodes.forEach(initializeWithin);
|
||||
});
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
const closeFromOutside = (event) => {
|
||||
tooltipStates.forEach((state) => {
|
||||
if (!state.wrap.contains(event.target) && !state.panel.contains(event.target)) {
|
||||
closeTooltip(state);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('pointerdown', closeFromOutside, true);
|
||||
document.addEventListener('touchstart', closeFromOutside, true);
|
||||
document.addEventListener('click', closeFromOutside, true);
|
||||
|
||||
const repositionOpenTooltips = () => {
|
||||
document.querySelectorAll('.sg-tooltip-wrap[data-tooltip-open="true"]').forEach((wrap) => {
|
||||
const state = states.get(wrap);
|
||||
if (state) {
|
||||
positionTooltip(state);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', repositionOpenTooltips);
|
||||
window.addEventListener('scroll', repositionOpenTooltips, true);
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
|
||||
} else {
|
||||
bootstrap();
|
||||
}
|
||||
})();
|
||||
@@ -3,6 +3,7 @@
|
||||
@import "./styles/03-typography-helpers.css";
|
||||
@import "./styles/10-components-interactive-elements.css";
|
||||
@import "./styles/47-patterns-file-upload.css";
|
||||
@import "./styles/48-patterns-history-graph.css";
|
||||
@import "./styles/20-patterns-portal-header.css";
|
||||
@import "./styles/21-patterns-options-row.css";
|
||||
@import "./styles/22-patterns-object-card.css";
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
--color-process-inactive: #FFAE79;
|
||||
--color-signal-green: #009101;
|
||||
--color-signal-green-light: #65A665;
|
||||
--color-signal-green-red: #C0624D;
|
||||
--color-signal-yellow: #9C7A00;
|
||||
--color-ampel-hellgelb: #D3B962;
|
||||
--color-signal-red: #9B3B2F;
|
||||
@@ -37,6 +38,7 @@
|
||||
--surface-card-header-muted: var(--color-darkbrown);
|
||||
--divider-card-segment: var(--color-white);
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-signal: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-dark);
|
||||
--text-card-transparent: var(--color-font-light);
|
||||
--layout-card-body-content-justify: flex-start;
|
||||
@@ -144,7 +146,6 @@
|
||||
--layout-preview-align-items: flex-start;
|
||||
|
||||
/* Semantic pattern tokens: Portal header */
|
||||
--surface-portal-background: var(--color-background-purple);
|
||||
--surface-portal-header: var(--color-darkblue);
|
||||
--surface-portal-header-tab: var(--color-midblue);
|
||||
--surface-portal-header-tab-active: var(--color-light-grey);
|
||||
@@ -310,7 +311,6 @@
|
||||
--dimension-input-label-width: 9rem;
|
||||
--dimension-input-field-desktop-width: 400px;
|
||||
--dimension-input-field-max-width: 600px;
|
||||
--dimension-email-body-max-width: 600px;
|
||||
--dimension-search-field-width: 15.3rem;
|
||||
--dimension-public-content-width: 100%;
|
||||
--dimension-layer-pulldown-panel: 40;
|
||||
|
||||
@@ -8,7 +8,7 @@ body {
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-base);
|
||||
color: var(--color-font-dark);
|
||||
background: var(--surface-portal-background);
|
||||
background: var(--color-background-purple);
|
||||
margin: 0;
|
||||
padding: var(--spacing-small);
|
||||
}
|
||||
|
||||
@@ -1240,9 +1240,7 @@
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel {
|
||||
@@ -1328,6 +1326,47 @@
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"] {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="bottom"]::before {
|
||||
top: calc(var(--spacing-small) * -1);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent var(--surface-tooltip);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="top"]::before {
|
||||
bottom: calc(var(--spacing-small) * -1);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: var(--surface-tooltip) transparent transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="left"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
right: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent transparent var(--surface-tooltip);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="right"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
left: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-pulldown-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + var(--spacing-small));
|
||||
|
||||
@@ -91,19 +91,39 @@
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left {
|
||||
order: 0;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-pulldown-demo,
|
||||
.sg-options-row__left > .sg-input-single-line-wrap,
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 1 1 calc(50% - var(--layout-options-row-group-gap));
|
||||
.sg-options-row__left > .sg-pulldown-demo {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left:has(> .sg-pulldown-demo ~ .sg-pulldown-demo) > .sg-pulldown-demo {
|
||||
flex-basis: calc(50% - (var(--layout-options-row-group-gap) / 2));
|
||||
}
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left > .sg-pulldown-demo {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 0 0 auto;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row > .sg-search-field-input {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row .sg-input-single-line {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-input-single-line-wrap .sg-input-single-line,
|
||||
|
||||
@@ -136,22 +136,27 @@
|
||||
|
||||
.sg-card-segment--signal-red {
|
||||
background: var(--color-signal-red);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green {
|
||||
background: var(--color-signal-green);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-light {
|
||||
background: var(--color-signal-green-light);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-red {
|
||||
background: var(--color-signal-green-red);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-yellow {
|
||||
background: var(--color-signal-yellow);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--gray {
|
||||
@@ -171,6 +176,11 @@
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card.sg-card--notification-signal-green-red {
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card--content-card {
|
||||
--surface-card-header-primary: var(--surface-content-block-card-title);
|
||||
--surface-card-body: var(--surface-content-block-card-content);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/* ========================================================= */
|
||||
/* Pattern: History Graph */
|
||||
/* ========================================================= */
|
||||
|
||||
.sg-history-graph {
|
||||
--layout-history-graph-connector-width: var(--score-marker-width);
|
||||
--surface-history-graph-connector: var(--chart-marker-line);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sg-history-graph__event {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__event:not(:last-child)::after {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
width: var(--layout-history-graph-connector-width);
|
||||
height: var(--spacing-large);
|
||||
background: var(--surface-history-graph-connector);
|
||||
content: "";
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-history-graph__card {
|
||||
width: 100%;
|
||||
max-width: var(--layout-object-card-min-width);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.sg-history-graph__navigation {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -78,10 +78,10 @@ function market_management_mcp_tools(): array
|
||||
['name' => 'market_management.update_market', 'description' => 'Markt inklusive Veranstalter und Website-URL ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer'], 'name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]],
|
||||
['name' => 'market_management.archive_market', 'description' => 'Markt archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer']]]],
|
||||
['name' => 'market_management.reactivate_market', 'description' => 'Markt reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer']]]],
|
||||
['name' => 'market_management.list_categories', 'description' => 'Bestehende Kategorien lesen', 'inputSchema' => ['type' => 'object', 'properties' => []]],
|
||||
['name' => 'market_management.list_market_organizers', 'description' => 'Aktive Veranstalter lesen', 'inputSchema' => ['type' => 'object', 'properties' => []]],
|
||||
['name' => 'market_management.list_categories', 'description' => 'Bestehende Kategorien lesen', 'inputSchema' => ['type' => 'object', 'properties' => (object) []]],
|
||||
['name' => 'market_management.list_market_organizers', 'description' => 'Aktive Veranstalter lesen', 'inputSchema' => ['type' => 'object', 'properties' => (object) []]],
|
||||
['name' => 'market_management.create_market_organizer', 'description' => 'Veranstalter mit optionalen Kontaktdaten anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name'], 'properties' => ['name' => ['type' => 'string'], 'email' => ['type' => ['string', 'null'], 'format' => 'email'], 'phone' => ['type' => ['string', 'null']]]]],
|
||||
['name' => 'market_management.list_reference_data', 'description' => 'Kategorien, Kantone und Gemeinden lesen', 'inputSchema' => ['type' => 'object', 'properties' => []]],
|
||||
['name' => 'market_management.list_reference_data', 'description' => 'Kategorien, Kantone und Gemeinden lesen', 'inputSchema' => ['type' => 'object', 'properties' => (object) []]],
|
||||
['name' => 'market_management.create_municipality', 'description' => 'Lokale Gemeinde im Markt-Gemeinderegister anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name'], 'properties' => ['name' => ['type' => 'string']]]],
|
||||
['name' => 'market_management.get_overview', 'description' => 'Marktübersicht mit serverseitigen Filtern lesen', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]],
|
||||
['name' => 'market_management.create_occurrence', 'description' => 'Marktdurchführung mit mindestens einem Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id', 'days'], 'properties' => ['market_definition_id' => ['type' => 'integer'], 'status' => ['type' => 'string'], 'days' => ['type' => 'array', 'minItems' => 1, 'items' => ['type' => 'object', 'required' => ['date'], 'properties' => $dayProperties]] + $occurrenceProperties]]],
|
||||
|
||||
@@ -18,6 +18,126 @@ const MARKET_OCCURRENCE_DAY_INHERITED_FIELDS = [
|
||||
'delivery_start_time', 'delivery_end_time',
|
||||
];
|
||||
|
||||
const MARKET_MANAGEMENT_MULTILINE_FIELDS = [
|
||||
'address', 'notes', 'other_infrastructure', 'own_stand_requirements',
|
||||
'travel_address', 'parking_options', 'delivery_access', 'storage_options',
|
||||
'travel_notes', 'cancellation_terms', 'revenue_share_terms',
|
||||
'other_participation_conditions', 'observations', 'cost_items_text',
|
||||
'description',
|
||||
];
|
||||
|
||||
function market_management_is_allowed_link_url(string $url): bool
|
||||
{
|
||||
$parts = parse_url($url);
|
||||
if (!is_array($parts)
|
||||
|| !in_array(($parts['scheme'] ?? ''), ['http', 'https'], true)
|
||||
|| ($parts['host'] ?? '') === ''
|
||||
|| isset($parts['user'], $parts['pass'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($parts['host'] ?? '') !== 'erpnaurua.imhochrain.ch' || ($parts['path'] ?? '') !== '/files.php') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isset($parts['port'], $parts['fragment']) || !preg_match('/^path=([^&]+)$/', (string) ($parts['query'] ?? ''), $matches)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$relativePath = rawurldecode($matches[1]);
|
||||
if ($relativePath === '' || str_contains($relativePath, "\0") || str_contains($relativePath, '\\') || str_starts_with($relativePath, '/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (explode('/', $relativePath) as $segment) {
|
||||
if ($segment === '' || $segment === '.' || $segment === '..') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function market_management_sanitize_multiline_html(?string $value): ?string
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$tokens = preg_split('/(<\/?a\b[^>]*>|<br\s*\/?>|<\/?strong\s*>)/iu', $value, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
if ($tokens === false) {
|
||||
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
$result = '';
|
||||
$anchorOpen = false;
|
||||
foreach ($tokens as $token) {
|
||||
if (preg_match('/^<br\s*\/?>$/iu', $token)) {
|
||||
$result .= '<br>';
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^<strong\s*>$/iu', $token)) {
|
||||
$result .= '<strong>';
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^<\/strong\s*>$/iu', $token)) {
|
||||
$result .= '</strong>';
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^<\/a\s*>$/iu', $token)) {
|
||||
if ($anchorOpen) {
|
||||
$result .= '</a>';
|
||||
$anchorOpen = false;
|
||||
} else {
|
||||
$result .= htmlspecialchars($token, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^<a\b[^>]*\bhref\s*=\s*(["\'])(.*?)\1[^>]*>$/isu', $token, $matches)
|
||||
&& market_management_is_allowed_link_url($matches[2])) {
|
||||
if ($anchorOpen) {
|
||||
$result .= '</a>';
|
||||
}
|
||||
$href = htmlspecialchars($matches[2], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$result .= '<a class="sg-hyperlink" href="' . $href . '" target="_blank" rel="noopener noreferrer">';
|
||||
$anchorOpen = true;
|
||||
continue;
|
||||
}
|
||||
$result .= htmlspecialchars($token, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
if ($anchorOpen) {
|
||||
$result .= '</a>';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
function market_management_sanitize_multiline_fields(array &$record): void
|
||||
{
|
||||
foreach (MARKET_MANAGEMENT_MULTILINE_FIELDS as $field) {
|
||||
if (array_key_exists($field, $record) && is_string($record[$field])) {
|
||||
$record[$field] = market_management_sanitize_multiline_html($record[$field]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function market_management_sanitize_occurrence_payload(array &$occurrence): void
|
||||
{
|
||||
market_management_sanitize_multiline_fields($occurrence);
|
||||
foreach ($occurrence['days'] ?? [] as &$day) {
|
||||
if (is_array($day)) {
|
||||
market_management_sanitize_multiline_fields($day);
|
||||
}
|
||||
}
|
||||
unset($day);
|
||||
foreach ($occurrence['cost_items'] ?? [] as &$costItem) {
|
||||
if (is_array($costItem)) {
|
||||
market_management_sanitize_multiline_fields($costItem);
|
||||
}
|
||||
}
|
||||
unset($costItem);
|
||||
}
|
||||
|
||||
function market_management_list_organizers(PDO $pdo): array
|
||||
{
|
||||
return $pdo->query(
|
||||
@@ -482,6 +602,7 @@ function market_management_get_market(PDO $pdo, int $marketId, bool $includeArch
|
||||
$costs = $pdo->prepare('SELECT * FROM market_occurrence_cost_item WHERE occurrence_id = :occurrence_id ORDER BY id ASC');
|
||||
$costs->execute([':occurrence_id' => $occurrenceId]);
|
||||
$occurrence['cost_items'] = $costs->fetchAll();
|
||||
market_management_sanitize_occurrence_payload($occurrence);
|
||||
}
|
||||
unset($occurrence);
|
||||
return $market;
|
||||
@@ -721,6 +842,9 @@ function market_management_write_fields(array $input, array $definitions): array
|
||||
'nullable_id' => market_management_optional_id($input, $field),
|
||||
default => throw new LogicException('Unbekannter Feldtyp.'),
|
||||
};
|
||||
if ($type === 'string' && in_array($field, MARKET_MANAGEMENT_MULTILINE_FIELDS, true)) {
|
||||
$value = market_management_sanitize_multiline_html($value);
|
||||
}
|
||||
$fields[] = $field . ' = :' . $field;
|
||||
$params[':' . $field] = $value;
|
||||
}
|
||||
@@ -982,6 +1106,7 @@ function market_management_get_occurrence(PDO $pdo, int $occurrenceId, bool $inc
|
||||
break;
|
||||
}
|
||||
}
|
||||
market_management_sanitize_occurrence_payload($occurrence);
|
||||
$costSummary = $pdo->prepare(
|
||||
'SELECT COALESCE(SUM(gross_amount), 0) AS gross_amount,
|
||||
COALESCE(SUM(net_amount), 0) AS net_amount,
|
||||
@@ -1330,7 +1455,7 @@ function market_management_create_cost_item(PDO $pdo, array $input): array
|
||||
$stmt->execute([
|
||||
':occurrence_id' => $occurrenceId,
|
||||
':cost_type_id' => $costTypeId,
|
||||
':description' => market_management_optional_string($input, 'description'),
|
||||
':description' => market_management_sanitize_multiline_html(market_management_optional_string($input, 'description')),
|
||||
':gross_amount' => $grossAmount,
|
||||
':tax_status' => $taxStatus,
|
||||
':tax_rate' => $taxRate,
|
||||
@@ -1381,7 +1506,9 @@ function market_management_update_cost_item(PDO $pdo, int $costItemId, array $in
|
||||
$sets[] = $field . ' = :' . $field;
|
||||
$params[':' . $field] = $type === 'number'
|
||||
? market_management_optional_number($input, $field)
|
||||
: market_management_optional_string($input, $field);
|
||||
: ($field === 'description'
|
||||
? market_management_sanitize_multiline_html(market_management_optional_string($input, $field))
|
||||
: market_management_optional_string($input, $field));
|
||||
}
|
||||
if (isset($params[':tax_status']) && !in_array($params[':tax_status'], ['tax_free', 'taxable'], true)) {
|
||||
throw new InvalidArgumentException('Ungültiger Steuerstatus.');
|
||||
@@ -1418,7 +1545,7 @@ function market_management_update_occurrence_simple_costs(PDO $pdo, int $occurre
|
||||
$item = market_management_create_cost_item($pdo, [
|
||||
'occurrence_id' => $occurrenceId,
|
||||
'cost_type_id' => market_management_get_cost_type_id($pdo, 'stand_fee'),
|
||||
'description' => array_key_exists('cost_items_text', $input) ? market_management_optional_string($input, 'cost_items_text') : null,
|
||||
'description' => array_key_exists('cost_items_text', $input) ? market_management_sanitize_multiline_html(market_management_optional_string($input, 'cost_items_text')) : null,
|
||||
'gross_amount' => $grossAmount,
|
||||
'tax_status' => $taxStatus,
|
||||
'tax_rate' => $taxRate,
|
||||
@@ -1429,7 +1556,7 @@ function market_management_update_occurrence_simple_costs(PDO $pdo, int $occurre
|
||||
$costFields['gross_amount'] = market_management_optional_number($input, 'gross_cost');
|
||||
}
|
||||
if (array_key_exists('cost_items_text', $input)) {
|
||||
$costFields['description'] = market_management_optional_string($input, 'cost_items_text');
|
||||
$costFields['description'] = market_management_sanitize_multiline_html(market_management_optional_string($input, 'cost_items_text'));
|
||||
}
|
||||
if (array_key_exists('tax_status', $input)) {
|
||||
$costFields['tax_status'] = trim((string) $input['tax_status']);
|
||||
|
||||
@@ -931,6 +931,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
|
||||
echo '</section>';
|
||||
echo '<script src="/scripts/help-icon-overlays.js"></script>';
|
||||
echo '<script src="/scripts/shared-editable-action-card.js?v=20260920-2"></script>';
|
||||
echo '<script src="/scripts/shared-mobile-detail.js?v=20260925-1"></script>';
|
||||
echo '<script>';
|
||||
echo 'const portalModuleNavigation = ' . $moduleNavigation . ';';
|
||||
echo 'const portalModuleContentCards = ' . $moduleContentCards . ';';
|
||||
@@ -1071,15 +1072,15 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
|
||||
echo " lagerBindingsInstalled = true;";
|
||||
echo " let lagerMobileDetailHost = null;";
|
||||
echo " let lagerMobileDetailOpen = false;";
|
||||
echo " const lagerMobileDetail = window.SharedMobileDetail?.create({ contentRoot, bodyClass: 'sg-lager-mobile-detail-open', hostClass: 'sg-lager-mobile-detail-host', hostDataAttribute: 'data-lager-mobile-detail-host', drawerDataAttribute: 'data-lager-mobile-drawer', backMarkup: '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\" aria-label=\"Navigation Detailansicht zurück oben\"><div class=\"sg-card-segment sg-card-segment--body\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-lager-detail-back>zurück</button></div></div></article></div></div>', historyKey: 'lagerView' });";
|
||||
echo " const renderLagerMobileDetail = (sourceDrawer, trigger) => { const type = trigger.dataset.lagerDrawerType || 'Lager'; const name = trigger.dataset.lagerDrawerName || ''; const value = trigger.dataset.lagerDrawerValue || ''; sourceDrawer.querySelector('[data-lager-drawer-title]').textContent = type + ': ' + name; sourceDrawer.querySelector('[data-lager-drawer-name-value]').textContent = name; sourceDrawer.querySelector('[data-lager-drawer-value-label]').textContent = type === 'Produkt' ? 'Anzahl im Lager:' : 'Artikelnummer:'; sourceDrawer.querySelector('[data-lager-drawer-value-value]').textContent = value; sourceDrawer.querySelectorAll('[data-lager-drawer-topic]').forEach((card) => { card.hidden = type !== 'Produkt'; }); const newLotForm = sourceDrawer.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); if (newLotForm) { newLotForm.hidden = true; } const newLotAction = sourceDrawer.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); if (newLotAction) { newLotAction.hidden = false; } if (type === 'Produkt') { const warning = trigger.dataset.lagerDrawerForecastWarning === 'true' || trigger.dataset.lagerDrawerForecastMhdWarning === 'true'; const warningValue = sourceDrawer.querySelector('[data-lager-drawer-warning-value]'); if (warningValue) { warningValue.textContent = warning ? (trigger.dataset.lagerDrawerForecast || 'Keine Prognose') : 'Keine akute Warnung.'; } const warningTitle = sourceDrawer.querySelector('[data-lager-drawer-warning-title]'); if (warningTitle) { warningTitle.textContent = getLagerWarningTitle(trigger); } sourceDrawer.querySelector('[data-lager-drawer-lot-number]').textContent = trigger.dataset.lagerDrawerLotNumber || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-current-qty]').textContent = trigger.dataset.lagerDrawerCurrentQty || '0'; sourceDrawer.querySelector('[data-lager-drawer-qty-in]').textContent = trigger.dataset.lagerDrawerQtyIn || '0'; sourceDrawer.querySelector('[data-lager-drawer-manufacturer-name]').textContent = trigger.dataset.lagerDrawerManufacturerName || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-manufacturer-lot-number]').textContent = trigger.dataset.lagerDrawerManufacturerLotNumber || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-mfg-date]').textContent = trigger.dataset.lagerDrawerMfgDate || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-expiry-date]').textContent = trigger.dataset.lagerDrawerExpiryDate || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-purchase-date]').textContent = trigger.dataset.lagerDrawerPurchaseDate || 'Nicht hinterlegt'; const productId = trigger.closest('[data-lager-row]')?.dataset.lagerId || ''; sourceDrawer.querySelectorAll('[data-lager-historical-product]').forEach((product) => { product.hidden = product.dataset.lagerHistoricalProduct !== productId; }); sourceDrawer.querySelectorAll('[data-lager-open-product]').forEach((product) => { product.hidden = product.dataset.lagerOpenProduct !== productId; product.querySelectorAll('[data-lager-open-edit-form]').forEach((form) => { form.hidden = true; }); product.querySelectorAll('[data-lager-open-card]').forEach((readView) => { readView.hidden = false; }); }); resetLagerNewLotConfirmation(); resetLagerOpenLotConfirmations(); sourceDrawer.dataset.lagerProductId = productId; populateLagerActiveLotForm(sourceDrawer, trigger); } const host = document.createElement('div'); host.className = 'sg-lager-mobile-detail-host'; host.dataset.lagerMobileDetailHost = 'true'; host.innerHTML = '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\" aria-label=\"Navigation Detailansicht zurück oben\"><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\" data-pattern-part=\"navigation-card-segment\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-lager-detail-back>zurück</button></div></div></article></div></div>'; const mobileDrawer = sourceDrawer; mobileDrawer.dataset.lagerMobileDrawer = 'true'; mobileDrawer.dataset.open = 'true'; mobileDrawer.setAttribute('aria-hidden', 'false'); host.appendChild(sourceDrawer); return host; };";
|
||||
echo " const restoreLagerMobileDetail = () => {";
|
||||
echo " if (lagerMobileDetailHost && lagerMobileDetailHost.parentNode) { const mobileDrawer = lagerMobileDetailHost.querySelector('[data-lager-mobile-drawer]'); if (mobileDrawer) { contentRoot.appendChild(mobileDrawer); mobileDrawer.removeAttribute('data-lager-mobile-drawer'); mobileDrawer.dataset.open = 'false'; mobileDrawer.setAttribute('aria-hidden', 'true'); } lagerMobileDetailHost.parentNode.removeChild(lagerMobileDetailHost); }";
|
||||
echo " lagerMobileDetailHost = null; lagerMobileDetailOpen = false; document.body.classList.remove('sg-lager-mobile-detail-open');";
|
||||
echo " lagerMobileDetail?.restore(); lagerMobileDetailHost = null; lagerMobileDetailOpen = false;";
|
||||
echo " };";
|
||||
echo " const openLagerMobileDetail = (trigger) => {";
|
||||
echo " restoreLagerMobileDetail(); const sourceDrawer = contentRoot.querySelector('[data-lager-drawer]'); lagerMobileDetailHost = sourceDrawer ? renderLagerMobileDetail(sourceDrawer, trigger) : null; const mobileDrawer = lagerMobileDetailHost?.querySelector('[data-lager-mobile-drawer]'); if (mobileDrawer) { const virtualLot = trigger.dataset.lagerDrawerVirtualLotNumber || ''; const virtualLotValue = mobileDrawer.querySelector('[data-lager-drawer-virtual-lot-value]'); if (virtualLotValue) { virtualLotValue.textContent = virtualLot ? 'Virtuelle Charge: ' + virtualLot : 'Keine virtuelle Fehlbestandscharge vorhanden.'; } }";
|
||||
echo " if (lagerMobileDetailHost) { document.body.appendChild(lagerMobileDetailHost); document.body.classList.add('sg-lager-mobile-detail-open'); }";
|
||||
echo " lagerMobileDetailOpen = true; window.history.pushState({ ...(window.history.state || {}), lagerView: 'detail' }, '', window.location.href);";
|
||||
echo " if (lagerMobileDetailHost && mobileDrawer) { lagerMobileDetail?.mount(mobileDrawer, trigger, lagerMobileDetailHost); if (window.SharedEditableActionCard) { window.SharedEditableActionCard.init(lagerMobileDetailHost); } }";
|
||||
echo " lagerMobileDetailOpen = true;";
|
||||
echo " };";
|
||||
echo " const closeLagerMobileDetail = () => {";
|
||||
echo " if (!lagerMobileDetailOpen) { return; }";
|
||||
@@ -1152,7 +1153,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
|
||||
echo " const getCurrentLimit = () => parseInt(getTable()?.dataset.marketsLimit || '20', 10);";
|
||||
echo " const getPageSize = () => parseInt(getTable()?.dataset.marketsPageSize || '20', 10);";
|
||||
echo " const marketEscape = (value) => String(value ?? '').replace(/[&<>\"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[character] || character));";
|
||||
echo " const marketDisplay = (value) => { if (value === null || value === undefined || value === '') { return 'Nicht hinterlegt'; } const escaped = marketEscape(value); const linked = escaped.replace(/https:\\/\\/erpnaurua\\.imhochrain\\.ch\\/files\\.php\\?path=[^\\s<]+/g, (url) => '<a class=\"sg-hyperlink\" href=\"' + url + '\" target=\"_blank\" rel=\"noopener\">' + url + '</a>'); return linked.replace(/\\r?\\n/g, '<br>'); };";
|
||||
echo " const marketDisplay = (value) => { if (value === null || value === undefined || value === '') { return 'Nicht hinterlegt'; } const escaped = marketEscape(value); const safeMarkup = escaped.replace(/<br>/g, '<br>').replace(/<strong>/g, '<strong>').replace(/<\\/strong>/g, '</strong>').replace(/<a class="sg-hyperlink" href="(https?:\\/\\/.*?)" target="_blank" rel="noopener noreferrer">/g, '<a class=\"sg-hyperlink\" href=\"$1\" target=\"_blank\" rel=\"noopener noreferrer\">').replace(/<\\/a>/g, '</a>'); return safeMarkup.replace(/\\r?\\n/g, '<br>'); };";
|
||||
echo " const marketBoolean = (value) => value === true || value === 'true' || value === 1 || value === '1' ? 'Ja' : value === false || value === 'false' || value === 0 || value === '0' ? 'Nein' : 'Nicht hinterlegt';";
|
||||
echo " const marketMoney = (value) => value === null || value === undefined || value === '' ? 'Nicht hinterlegt' : 'CHF ' + Number(value).toFixed(2);";
|
||||
echo " const marketContactName = (contact) => { if (!contact) { return 'Nicht hinterlegt'; } return marketDisplay([contact.first_name, contact.last_name].filter(Boolean).join(' ')); };";
|
||||
@@ -1180,9 +1181,10 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
|
||||
echo " const syncSearchState = () => { const input = getSearchInput(); const wrap = input?.closest('[data-component=\'single-line-input\']'); if (!input || !wrap) { return; } const hasValue = input.value.trim() !== ''; wrap.setAttribute('data-has-value', String(hasValue)); wrap.dataset.componentState = hasValue ? 'active' : 'inactive-selectable'; };";
|
||||
echo " const bindTable = () => { const table = getTable(); if (!table) { return; } const input = getSearchInput(); const clearButton = table.querySelector('[data-markets-search-clear]'); if (input && input.dataset.marketsBound !== 'true') { input.dataset.marketsBound = 'true'; input.addEventListener('input', () => { syncSearchState(); if (searchTimer) { window.clearTimeout(searchTimer); } searchTimer = window.setTimeout(() => { void loadFragment({ market_search: getSearchValue(), market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getPageSize() }); }, 500); }); } if (clearButton && clearButton.dataset.marketsBound !== 'true') { clearButton.dataset.marketsBound = 'true'; clearButton.addEventListener('click', () => { const currentInput = getSearchInput(); if (currentInput) { currentInput.value = ''; } syncSearchState(); void loadFragment({ market_search: '', market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getPageSize() }); }); } syncSearchState(); };";
|
||||
echo " let marketMobileDetailHost = null; let marketOpeningMobileDetail = false; let marketDetailOpen = false;";
|
||||
echo " const marketMobileDetail = window.SharedMobileDetail?.create({ contentRoot, bodyClass: 'sg-market-mobile-detail-open', hostClass: 'sg-market-mobile-detail-host', hostDataAttribute: 'data-market-mobile-detail-host', drawerDataAttribute: 'data-market-mobile-drawer', backMarkup: '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\"><div class=\"sg-card-segment sg-card-segment--body\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-market-detail-back>zurück</button></div></div></article></div></div>', historyKey: 'marketView' });";
|
||||
echo " const getMarketDrawer = () => document.querySelector('[data-market-mobile-drawer]') || contentRoot.querySelector('[data-market-drawer]');";
|
||||
echo " const getMarketBackdrop = () => contentRoot.querySelector('[data-market-drawer-backdrop]');";
|
||||
echo " const setMarketDrawerState = (open) => { const drawer = getMarketDrawer(); const backdrop = getMarketBackdrop(); if (!drawer || !backdrop) { return; } drawer.dataset.open = String(open); drawer.setAttribute('aria-hidden', String(!open)); backdrop.dataset.open = String(open); backdrop.setAttribute('aria-hidden', String(!open)); document.body.classList.toggle('sg-card-list-page-drawer-open', open); };";
|
||||
echo " const setMarketDrawerState = (open) => { const drawer = getMarketDrawer(); const backdrop = getMarketBackdrop(); if (!drawer || !backdrop) { return; } const drawerOpen = Boolean(open); const backdropOpen = drawerOpen && !marketDetailOpen; drawer.dataset.open = String(drawerOpen); drawer.setAttribute('aria-hidden', String(!drawerOpen)); backdrop.dataset.open = String(backdropOpen); backdrop.setAttribute('aria-hidden', String(!backdropOpen)); document.body.classList.toggle('sg-card-list-page-drawer-open', backdropOpen); };";
|
||||
echo " const applyMarketTabs = (root) => { const group = root.querySelector('[data-pattern=\"card-gruppe-mit-tastennavigation\"] [role=\"tablist\"]'); if (group) { group.dataset.component = 'tab-navigation'; group.dataset.componentSize = 'large'; group.querySelectorAll('[data-market-day-tab]').forEach((tab) => { tab.dataset.componentPart = 'tab-button'; tab.classList.remove('sg-button--active'); }); } root.querySelectorAll('[data-market-day-tab]').forEach((tab) => { if (tab.dataset.marketTabBound === 'true') { return; } tab.dataset.marketTabBound = 'true'; tab.addEventListener('click', () => { const tabGroup = tab.closest('[data-pattern=\"card-gruppe-mit-tastennavigation\"]'); tabGroup?.querySelectorAll('[data-market-day-tab]').forEach((candidate) => candidate.setAttribute('aria-selected', String(candidate === tab))); tabGroup?.querySelectorAll('[role=\"tabpanel\"]').forEach((panel) => { panel.hidden = panel.id !== tab.getAttribute('aria-controls'); }); tab.focus(); }); }); };";
|
||||
echo " const syncMarketDayAddButton = (root = contentRoot) => { if (!root.querySelector('[data-market-inherit-row]')) { enhanceMarketDayInheritance(root); } const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; const days = Array.isArray(payload?.occurrence?.days) ? payload.occurrence.days.filter((day) => !day.__draft).slice().sort((left, right) => String(left.date).localeCompare(String(right.date))) : []; const nextDate = (date) => { const value = new Date(String(date) + 'T00:00:00Z'); value.setUTCDate(value.getUTCDate() + 1); return value.toISOString().slice(0, 10); }; root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=\"day\"]').forEach((form) => { const input = form.querySelector('[name=\"date\"]'); if (!input) { return; } const index = days.findIndex((day) => String(day.id) === String(form.dataset.marketEditId)); const minimum = form.dataset.marketEditDraft === 'true' ? days.at(-1)?.date : index > 0 ? days[index - 1].date : ''; input.min = minimum ? nextDate(minimum) : ''; }); const addButton = root.querySelector('[data-market-day-add]'); if (!addButton) { return; } const editing = Array.from(root.querySelectorAll('[data-market-edit-form]')).some((form) => form.closest('[data-shared-editable-card-form]')?.hidden === false); addButton.disabled = editing; addButton.setAttribute('aria-disabled', String(editing)); addButton.classList.toggle('sg-button--active', !editing); addButton.classList.toggle('sg-button--inactive', editing); };";
|
||||
echo " const enhanceMarketDayInheritance = (root) => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; const days = Array.isArray(payload?.occurrence?.days) ? payload.occurrence.days.filter((day) => !day.__draft).slice().sort((left, right) => String(left.date).localeCompare(String(right.date))) : []; const firstDayId = days[0]?.id; root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=\"day\"]').forEach((form) => { const dayId = form.dataset.marketEditId; if (!dayId || String(dayId) === String(firstDayId)) { return; } const dateGroup = form.querySelector('[name=\"date\"]')?.closest('.sg-form-sections-card__field-group'); const observationsGroup = form.querySelector('[name=\"observations\"]')?.closest('.sg-form-sections-card__field-group'); if (!dateGroup || !observationsGroup) { return; } let row = form.querySelector('[data-market-inherit-row]'); if (!row) { row = document.createElement('div'); row.className = 'sg-form-sections-card__field-group'; row.dataset.marketInheritRow = 'true'; row.innerHTML = '<label class=\"sg-checkbox-field-option sg-body\" data-component=\"checkbox-field\" data-component-state=\"default\"><button class=\"sg-checkbox-field\" type=\"button\" role=\"checkbox\" aria-checked=\"false\" aria-label=\"Ab hier alle Werte aus Tag 1 übernehmen\" data-market-inherit-toggle><span class=\"sg-checkbox-field__mark\" aria-hidden=\"true\">✓</span></button><span>Ab hier alle Werte aus Tag 1 übernehmen</span><input type=\"hidden\" name=\"inherit_from_first_day\" value=\"0\"></label>'; observationsGroup.after(row); row.querySelector('[data-market-inherit-toggle]').addEventListener('click', (event) => { event.stopImmediatePropagation(); const toggle = row.querySelector('[data-market-inherit-toggle]'); const checked = toggle.getAttribute('aria-checked') !== 'true'; toggle.setAttribute('aria-checked', String(checked)); toggle.classList.toggle('sg-form-active', checked); row.querySelector('[name=\"inherit_from_first_day\"]').value = checked ? '1' : '0'; form.querySelectorAll('input, select, textarea').forEach((control) => { if (control.name !== 'date' && control.name !== 'inherit_from_first_day' && control.name !== 'observations') { control.disabled = checked; control.setAttribute('aria-disabled', String(checked)); control.classList.toggle('sg-form-disabled', checked); control.classList.toggle('sg-input-single-line--disabled', checked && control.tagName === 'INPUT'); } }); syncMarketDayAddButton(root); }); } const day = days.find((candidate) => String(candidate.id) === String(dayId)); const inherited = Boolean(day?.inherit_from_first_day); const toggle = row.querySelector('[data-market-inherit-toggle]'); toggle.setAttribute('aria-checked', String(inherited)); toggle.classList.toggle('sg-form-active', inherited); row.querySelector('[name=\"inherit_from_first_day\"]').value = inherited ? '1' : '0'; form.querySelectorAll('input, select, textarea').forEach((control) => { if (control.name !== 'date' && control.name !== 'inherit_from_first_day' && control.name !== 'observations') { control.disabled = inherited; control.setAttribute('aria-disabled', String(inherited)); control.classList.toggle('sg-form-disabled', inherited); control.classList.toggle('sg-input-single-line--disabled', inherited && control.tagName === 'INPUT'); } }); }); };";
|
||||
@@ -1197,8 +1199,8 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
|
||||
echo " const marketDrawerTitle = (payload) => { const marketName = payload.market?.name || 'Marktdurchführung'; const dates = (Array.isArray(payload.occurrence?.days) ? payload.occurrence.days : []).map((day) => marketDate(day.date)).filter(Boolean); return dates.length ? marketName + ', Marktdurchführung ' + dates.join(', ') : marketName; };";
|
||||
echo " const openMarketDrawer = async (trigger) => { const drawer = getMarketDrawer(); if (!drawer) { return; } const title = drawer.querySelector('[data-market-drawer-title]'); const content = drawer.querySelector('[data-market-drawer-content]'); setMarketDrawerState(true); if (title) { title.textContent = 'Marktdurchführung'; } if (content) { content.innerHTML = '<p class=\"sg-body\">Marktdetails werden geladen …</p>'; } try { const response = await fetch('/api/market-occurrence.php?id=' + encodeURIComponent(trigger.dataset.marketOccurrenceId || ''), { credentials: 'same-origin' }); const payload = await response.json(); if (!response.ok || !payload.ok) { throw new Error(payload.error || 'Marktdetails konnten nicht geladen werden.'); } drawer.dataset.marketPayload = JSON.stringify(payload); if (title) { title.textContent = marketDrawerTitle(payload); } if (content) { content.innerHTML = renderMarketDetailWithActions(payload); applyMarketTabs(content); syncMarketDayAddButton(content); } } catch (error) { if (content) { content.innerHTML = '<p class=\"sg-body\">' + marketEscape(error.message) + '</p>'; } } contentRoot.querySelectorAll('[data-market-drawer-open]').forEach((button) => button.setAttribute('aria-expanded', String(button === trigger))); };";
|
||||
echo " const openNewMarketDrawer = async () => { const drawer = getMarketDrawer(); if (!drawer) { return; } const title = drawer.querySelector('[data-market-drawer-title]'); const content = drawer.querySelector('[data-market-drawer-content]'); setMarketDrawerState(true); if (title) { title.textContent = 'Neue Marktdurchführung'; } if (content) { content.innerHTML = '<p class=\"sg-body\">Pflichtangaben werden geladen …</p>'; } try { const response = await fetch('/api/market-occurrence.php?action=create_options', { credentials: 'same-origin' }); const payload = await response.json(); if (!response.ok || !payload.ok) { throw new Error(payload.error || 'Pflichtangaben konnten nicht geladen werden.'); } if (content) { content.innerHTML = marketCreateForm(payload.market_definitions || []); } } catch (error) { if (content) { content.innerHTML = '<p class=\"sg-body\">' + marketEscape(error.message) + '</p>'; } } };";
|
||||
echo " const restoreMarketList = () => { if (marketMobileDetailHost?.parentNode) { const drawer = marketMobileDetailHost.querySelector('[data-market-mobile-drawer]'); if (drawer) { contentRoot.appendChild(drawer); drawer.removeAttribute('data-market-mobile-drawer'); drawer.dataset.open = 'false'; drawer.setAttribute('aria-hidden', 'true'); } marketMobileDetailHost.parentNode.removeChild(marketMobileDetailHost); } marketMobileDetailHost = null; marketDetailOpen = false; document.body.classList.remove('sg-market-mobile-detail-open'); bindTable(); };";
|
||||
echo " const openMarketMobileDetail = (trigger) => { restoreMarketList(); const sourceDrawer = contentRoot.querySelector('[data-market-drawer]'); if (!sourceDrawer) { return; } const host = document.createElement('div'); host.className = 'sg-market-mobile-detail-host'; host.dataset.marketMobileDetailHost = 'true'; host.innerHTML = '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\"><div class=\"sg-card-segment sg-card-segment--body\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-market-detail-back>zurück</button></div></div></article></div></div>'; sourceDrawer.dataset.marketMobileDrawer = 'true'; sourceDrawer.dataset.open = 'true'; sourceDrawer.setAttribute('aria-hidden', 'false'); host.appendChild(sourceDrawer); document.body.appendChild(host); marketMobileDetailHost = host; document.body.classList.add('sg-market-mobile-detail-open'); marketOpeningMobileDetail = true; void openMarketDrawer(trigger).finally(() => { marketOpeningMobileDetail = false; }); marketDetailOpen = true; window.history.pushState({ marketView: 'detail' }, '', window.location.href); };";
|
||||
echo " const restoreMarketList = () => { marketMobileDetail?.restore(); marketMobileDetailHost = null; marketDetailOpen = false; bindTable(); };";
|
||||
echo " const openMarketMobileDetail = (trigger) => { restoreMarketList(); const sourceDrawer = contentRoot.querySelector('[data-market-drawer]'); if (!sourceDrawer) { return; } const host = document.createElement('div'); host.className = 'sg-market-mobile-detail-host'; host.dataset.marketMobileDetailHost = 'true'; host.innerHTML = '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\"><div class=\"sg-card-segment sg-card-segment--body\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-market-detail-back>zurück</button></div></div></article></div></div>'; sourceDrawer.dataset.marketMobileDrawer = 'true'; sourceDrawer.dataset.open = 'true'; sourceDrawer.setAttribute('aria-hidden', 'false'); host.appendChild(sourceDrawer); marketMobileDetailHost = host; marketDetailOpen = true; marketMobileDetail?.mount(sourceDrawer, trigger, host); if (window.SharedEditableActionCard) { window.SharedEditableActionCard.init(host); } marketOpeningMobileDetail = true; void openMarketDrawer(trigger).finally(() => { marketOpeningMobileDetail = false; }); };";
|
||||
echo " const deleteMarketOccurrence = async (submit) => { if (submit.disabled) { return; } const card = submit.closest('[data-shared-editable-action-card]'); const confirmation = Array.from(card?.children || []).find((child) => child.matches('[data-shared-editable-card-delete-confirmation]')); const input = confirmation?.querySelector('[data-shared-editable-card-delete-input]'); const status = confirmation?.querySelector('[data-market-delete-status]'); const occurrenceId = card?.dataset.marketDeleteOccurrenceId || ''; if (!card || !input || input.value !== 'DELETE' || !occurrenceId) { return; } if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'delete', id: occurrenceId, confirmation: input.value }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Löschen fehlgeschlagen.'); } closeMarketDrawer(); if (marketDetailOpen) { restoreMarketList(); } await loadFragment({ market_search: getSearchValue(), market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getCurrentLimit() }); } catch (error) { if (status) { status.textContent = marketEscape(error.message); } submit.disabled = false; submit.setAttribute('aria-disabled', 'false'); } };";
|
||||
echo " const deleteMarketDay = async (submit) => { if (submit.disabled) { return; } const card = submit.closest('[data-shared-editable-action-card]'); const input = card?.querySelector('[data-shared-editable-card-delete-input]'); const status = card?.querySelector('[data-market-day-delete-status]'); const dayId = card?.dataset.marketDayDeleteId || ''; if (!card || !input || input.value !== 'DELETE' || !dayId) { return; } if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'delete_day', id: dayId, confirmation: input.value }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Löschen fehlgeschlagen.'); } const trigger = contentRoot.querySelector('[data-market-drawer-open][aria-expanded=\"true\"]'); if (trigger) { await openMarketDrawer(trigger); } } catch (error) { if (status) { status.textContent = marketEscape(error.message); } submit.disabled = false; submit.setAttribute('aria-disabled', 'false'); } };";
|
||||
echo " const marketDayDeleteObserver = new MutationObserver(() => enhanceMarketDayDeleteControls(contentRoot)); marketDayDeleteObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
@@ -1241,6 +1243,8 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
|
||||
echo " if (!contentRoot || !definitionContent || !drawer || !readCard || !card || !form || !editButton || definitionContent.dataset.marketDefinitionsBound === 'true') { return; }";
|
||||
echo " definitionContent.dataset.marketDefinitionsBound = 'true';";
|
||||
echo " let activeMarket = null;";
|
||||
echo " let marketDefinitionMobileHost = null; let marketDefinitionMobileOpen = false;";
|
||||
echo " const marketDefinitionMobileDetail = window.SharedMobileDetail?.create({ contentRoot, bodyClass: 'sg-market-definition-mobile-detail-open', hostClass: 'sg-market-definition-mobile-detail-host', hostDataAttribute: 'data-market-definition-mobile-detail-host', drawerDataAttribute: 'data-market-definition-mobile-drawer', backMarkup: '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\"><div class=\"sg-card-segment sg-card-segment--body\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-market-definition-detail-back>zurück</button></div></div></article></div></div>', historyKey: 'marketDefinitionView' });";
|
||||
echo <<<JS
|
||||
const prepareMarketDefinitionDeleteCard = () => {
|
||||
const existingCard = drawer.querySelector('[data-market-definition-card]');
|
||||
@@ -1299,13 +1303,15 @@ JS;
|
||||
echo " prepareMarketDefinitionDeleteCard();";
|
||||
echo " const sharedCard = drawer.querySelector('[data-market-definition-card]');";
|
||||
echo " if (window.SharedEditableActionCard) { window.SharedEditableActionCard.init(definitionContent); }";
|
||||
echo " const setDrawerState = (open) => { drawer.dataset.open = String(open); drawer.setAttribute('aria-hidden', String(!open)); if (activeMarket?.trigger) { activeMarket.trigger.setAttribute('aria-expanded', String(open)); } const backdrop = definitionContent.querySelector('[data-market-definition-drawer-backdrop]'); if (backdrop) { backdrop.dataset.open = String(open); backdrop.setAttribute('aria-hidden', String(!open)); } document.body.classList.toggle('sg-card-list-page-drawer-open', open); };";
|
||||
echo " const setDrawerState = (open) => { const drawerOpen = Boolean(open); if (!drawerOpen && marketDefinitionMobileOpen) { marketDefinitionMobileDetail?.restore(); marketDefinitionMobileHost = null; marketDefinitionMobileOpen = false; } const backdropOpen = drawerOpen && !marketDefinitionMobileOpen; drawer.dataset.open = String(drawerOpen); drawer.setAttribute('aria-hidden', String(!drawerOpen)); if (activeMarket?.trigger) { activeMarket.trigger.setAttribute('aria-expanded', String(drawerOpen)); } const backdrop = definitionContent.querySelector('[data-market-definition-drawer-backdrop]'); if (backdrop) { backdrop.dataset.open = String(backdropOpen); backdrop.setAttribute('aria-hidden', String(!backdropOpen)); } document.body.classList.toggle('sg-card-list-page-drawer-open', backdropOpen); };";
|
||||
echo " const restoreMarketDefinitionMobileDetail = () => { marketDefinitionMobileDetail?.restore(); marketDefinitionMobileHost = null; marketDefinitionMobileOpen = false; };";
|
||||
echo " const openMarketDefinitionMobileDetail = (trigger) => { restoreMarketDefinitionMobileDetail(); const sourceDrawer = drawer; const host = document.createElement('div'); host.className = 'sg-market-definition-mobile-detail-host'; host.dataset.marketDefinitionMobileDetailHost = 'true'; sourceDrawer.dataset.marketDefinitionMobileDrawer = 'true'; sourceDrawer.dataset.open = 'true'; sourceDrawer.setAttribute('aria-hidden', 'false'); host.appendChild(sourceDrawer); marketDefinitionMobileHost = host; marketDefinitionMobileOpen = true; marketDefinitionMobileDetail?.mount(sourceDrawer, trigger, host); if (window.SharedEditableActionCard) { window.SharedEditableActionCard.init(host); } showReadCard(trigger); };";
|
||||
echo " const resetForm = () => { form.reset(); const status = form.querySelector('[data-market-definition-status]'); if (status) { status.textContent = ''; } syncNewOrganizerFields(); };";
|
||||
echo " const syncNewOrganizerFields = () => { const select = form.querySelector('[data-market-definition-organizer]'); const fields = form.querySelector('[data-market-definition-new-organizer]'); if (!select || !fields) { return; } fields.hidden = select.value !== '__new__'; };";
|
||||
echo " const closeDrawer = () => { resetForm(); if (sharedCard && window.SharedEditableActionCard) { window.SharedEditableActionCard.resetDeleteConfirmation(sharedCard); window.SharedEditableActionCard.setEditMode(sharedCard, false); } activeMarket = null; setDrawerState(false); };";
|
||||
echo " const deleteMarketDefinition = async (submit) => { if (submit.disabled || !activeMarket?.id) { return; } const card = submit.closest('[data-shared-editable-action-card]'); const input = card?.querySelector('[data-shared-editable-card-delete-input]'); const status = card?.querySelector('[data-market-definition-delete-status]'); if (!card || !input || input.value !== 'DELETE') { return; } if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); try { const response = await fetch('/api/market-definition.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': definitionContent.dataset.marketDefinitionsCsrfToken || '' }, body: JSON.stringify({ action: 'delete', id: activeMarket.id, confirmation: input.value }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Löschen fehlgeschlagen.'); } const row = activeMarket.trigger.closest('[data-market-definition-row]'); row?.remove(); closeDrawer(); } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Löschen fehlgeschlagen.'; } submit.disabled = false; submit.setAttribute('aria-disabled', 'false'); } };";
|
||||
echo " const showReadCard = (trigger) => { activeMarket = { id: trigger.dataset.marketDefinitionId || '', trigger }; drawer.querySelector('[data-market-definition-read-title]').textContent = trigger.dataset.marketDefinitionName || 'Markt'; readCard.querySelector('[data-market-definition-read-name]').textContent = trigger.dataset.marketDefinitionName || 'Nicht hinterlegt'; readCard.querySelector('[data-market-definition-read-category]').textContent = trigger.dataset.marketDefinitionCategoryName || 'Nicht hinterlegt'; readCard.querySelector('[data-market-definition-read-organizer]').textContent = trigger.dataset.marketDefinitionOrganizerName || 'Nicht hinterlegt'; readCard.querySelector('[data-market-definition-read-website]').textContent = trigger.dataset.marketDefinitionWebsite || 'Nicht hinterlegt'; const title = form.querySelector('[data-market-definition-form-title]'); if (title) { title.textContent = 'Markt bearbeiten'; } const name = form.querySelector('[data-market-definition-name]'); const category = form.querySelector('[data-market-definition-category]'); const organizer = form.querySelector('[data-market-definition-organizer]'); const website = form.querySelector('[data-market-definition-website]'); if (name) { name.value = trigger.dataset.marketDefinitionName || ''; } if (category) { category.value = trigger.dataset.marketDefinitionCategoryId || ''; } if (organizer) { organizer.value = trigger.dataset.marketDefinitionOrganizerId || ''; } if (website) { website.value = trigger.dataset.marketDefinitionWebsite === 'Nicht hinterlegt' ? '' : (trigger.dataset.marketDefinitionWebsite || ''); } syncNewOrganizerFields(); if (sharedCard && window.SharedEditableActionCard) { window.SharedEditableActionCard.setEditMode(sharedCard, false); } setDrawerState(true); };";
|
||||
echo " definitionContent.addEventListener('click', (event) => { const trigger = event.target.closest('[data-market-definition-open]'); if (!trigger || !definitionContent.contains(trigger)) { return; } event.preventDefault(); event.stopPropagation(); showReadCard(trigger); }, true);";
|
||||
echo " definitionContent.addEventListener('click', (event) => { const trigger = event.target.closest('[data-market-definition-open]'); if (!trigger || !definitionContent.contains(trigger)) { return; } event.preventDefault(); event.stopPropagation(); if (window.matchMedia('(max-width: 767px)').matches) { openMarketDefinitionMobileDetail(trigger); } else { showReadCard(trigger); } }, true);";
|
||||
echo " definitionContent.addEventListener('click', (event) => { const deleteSubmit = event.target.closest('[data-market-definition-delete-submit]'); if (deleteSubmit) { void deleteMarketDefinition(deleteSubmit); return; } const trigger = event.target.closest('[data-market-definition-open]'); if (trigger) { const deleteButton = drawer.querySelector('[data-market-definition-delete]'); if (deleteButton) { deleteButton.hidden = false; } } if (event.target.closest('[data-market-definition-create]')) { const deleteButton = drawer.querySelector('[data-market-definition-delete]'); if (deleteButton) { deleteButton.hidden = true; } } });";
|
||||
echo " const openEditCard = () => { if (!activeMarket) { return; } const title = form.querySelector('[data-market-definition-form-title]'); if (title) { title.textContent = 'Markt bearbeiten'; } const name = form.querySelector('[data-market-definition-name]'); const category = form.querySelector('[data-market-definition-category]'); const organizer = form.querySelector('[data-market-definition-organizer]'); const website = form.querySelector('[data-market-definition-website]'); if (name) { name.value = activeMarket.trigger.dataset.marketDefinitionName || ''; } if (category) { category.value = activeMarket.trigger.dataset.marketDefinitionCategoryId || ''; } if (organizer) { organizer.value = activeMarket.trigger.dataset.marketDefinitionOrganizerId || ''; } if (website) { website.value = activeMarket.trigger.dataset.marketDefinitionWebsite === 'Nicht hinterlegt' ? '' : (activeMarket.trigger.dataset.marketDefinitionWebsite || ''); } syncNewOrganizerFields(); };";
|
||||
echo " editButton.addEventListener('click', (event) => { event.preventDefault(); openEditCard(); });";
|
||||
@@ -1313,6 +1319,8 @@ JS;
|
||||
echo " const appendMarketRow = (market) => { const table = contentRoot.querySelector('[data-market-definitions-table]'); if (!table) { return; } const row = document.createElement('div'); row.className = 'sg-card-segment sg-card-segment--body sg-large-table__row'; row.setAttribute('role', 'row'); row.dataset.componentPart = 'large-table-row'; row.dataset.marketDefinitionRow = ''; row.dataset.marketDefinitionId = String(market.id || ''); const category = form.querySelector('[data-market-definition-category]')?.selectedOptions[0]?.textContent || ''; const organizer = form.querySelector('[data-market-definition-organizer]')?.selectedOptions[0]?.textContent || ''; const website = market.website_url || 'Nicht hinterlegt'; const cells = document.createElement('div'); cells.className = 'sg-large-table__cell'; const trigger = document.createElement('a'); trigger.className = 'sg-hyperlink'; trigger.href = '#'; trigger.setAttribute('aria-haspopup', 'dialog'); trigger.setAttribute('aria-expanded', 'false'); trigger.dataset.marketDefinitionOpen = ''; trigger.dataset.marketDefinitionId = String(market.id || ''); trigger.dataset.marketDefinitionName = market.name || ''; trigger.dataset.marketDefinitionCategoryId = String(market.category_id || ''); trigger.dataset.marketDefinitionCategoryName = category; trigger.dataset.marketDefinitionOrganizerId = String(market.organizer_id || ''); trigger.dataset.marketDefinitionOrganizerName = organizer; trigger.dataset.marketDefinitionWebsite = website; trigger.textContent = market.name || ''; cells.appendChild(trigger); row.appendChild(cells); [category, organizer, website, '0', 'Aktiv'].forEach((value) => { const cell = document.createElement('div'); cell.className = 'sg-large-table__cell'; cell.setAttribute('role', 'cell'); cell.textContent = value; row.appendChild(cell); }); table.appendChild(row); };";
|
||||
echo " document.addEventListener('click', (event) => { if (event.target.closest('[data-market-definition-create]')) { event.preventDefault(); activeMarket = null; resetForm(); const title = form.querySelector('[data-market-definition-form-title]'); if (title) { title.textContent = 'Neuen Markt anlegen'; } if (sharedCard && window.SharedEditableActionCard) { window.SharedEditableActionCard.setEditMode(sharedCard, true); } setDrawerState(true); form.querySelector('[data-market-definition-name]')?.focus(); return; } if (event.target.closest('[data-market-definition-edit]')) { event.preventDefault(); openEditCard(); return; } if (event.target.closest('[data-market-definition-cancel]')) { event.preventDefault(); if (!activeMarket) { closeDrawer(); } return; } if (event.target.closest('[data-market-definition-drawer-backdrop]')) { event.preventDefault(); closeDrawer(); } });";
|
||||
echo " document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && drawer.dataset.open === 'true') { closeDrawer(); } });";
|
||||
echo " document.addEventListener('click', (event) => { if (event.target.closest('[data-market-definition-detail-back]')) { event.preventDefault(); if (window.history.state?.marketDefinitionView === 'detail') { window.history.back(); } else { restoreMarketDefinitionMobileDetail(); } } });";
|
||||
echo " window.addEventListener('popstate', (event) => { if (marketDefinitionMobileOpen && (!event.state || event.state.marketDefinitionView !== 'detail')) { restoreMarketDefinitionMobileDetail(); } });";
|
||||
echo " form.addEventListener('submit', async (event) => { event.preventDefault(); const status = form.querySelector('[data-market-definition-status]'); const submit = form.querySelector('[type=submit]'); const fields = Object.fromEntries(new FormData(form).entries()); const isUpdate = Boolean(activeMarket?.id); if (fields.organizer_id === '__new__' && !String(fields.organizer_name || '').trim()) { if (status) { status.textContent = 'Name des neuen Veranstalters ist erforderlich.'; } return; } if (status) { status.textContent = 'Speichern läuft …'; } if (submit) { submit.disabled = true; } try { const response = await fetch('/api/market-definition.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': definitionContent.dataset.marketDefinitionsCsrfToken || '' }, body: JSON.stringify({ action: isUpdate ? 'update' : 'create', id: isUpdate ? activeMarket.id : null, fields }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } if (isUpdate) { const trigger = activeMarket.trigger; const category = form.querySelector('[data-market-definition-category]')?.selectedOptions[0]; const organizer = form.querySelector('[data-market-definition-organizer]')?.selectedOptions[0]; const website = result.market.website_url || 'Nicht hinterlegt'; trigger.textContent = result.market.name; trigger.dataset.marketDefinitionName = result.market.name; trigger.dataset.marketDefinitionCategoryId = String(result.market.category_id); trigger.dataset.marketDefinitionCategoryName = category?.textContent || ''; trigger.dataset.marketDefinitionOrganizerId = String(result.market.organizer_id); trigger.dataset.marketDefinitionOrganizerName = organizer?.textContent || ''; trigger.dataset.marketDefinitionWebsite = website; showReadCard(trigger); } else { appendMarketRow(result.market); closeDrawer(); } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'; } } finally { if (submit) { submit.disabled = false; } } });";
|
||||
echo "}";
|
||||
echo "function initBestellungenBindings() {";
|
||||
|
||||
@@ -4,6 +4,19 @@ declare(strict_types=1);
|
||||
require_once __DIR__ . '/../../modules/shared/db.php';
|
||||
require_once __DIR__ . '/../../modules/market_management/mcp.php';
|
||||
|
||||
final class MarketManagementMcpJsonRpcException extends RuntimeException
|
||||
{
|
||||
public function __construct(private int $rpcCode, string $message, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
|
||||
public function rpcCode(): int
|
||||
{
|
||||
return $this->rpcCode;
|
||||
}
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
@@ -18,6 +31,10 @@ try {
|
||||
throw new InvalidArgumentException('Ungültige JSON-RPC-Anfrage.');
|
||||
}
|
||||
|
||||
if (array_key_exists('params', $request) && !is_array($request['params'])) {
|
||||
throw new MarketManagementMcpJsonRpcException(-32602, 'Parameter müssen als Objekt oder Array übergeben werden.');
|
||||
}
|
||||
|
||||
$id = $request['id'] ?? null;
|
||||
$method = (string) ($request['method'] ?? '');
|
||||
$params = is_array($request['params'] ?? null) ? $request['params'] : [];
|
||||
@@ -36,23 +53,35 @@ try {
|
||||
} elseif ($method === 'tools/call') {
|
||||
$tool = (string) ($params['name'] ?? '');
|
||||
$arguments = is_array($params['arguments'] ?? null) ? $params['arguments'] : [];
|
||||
$result = [
|
||||
'content' => [[
|
||||
'type' => 'text',
|
||||
'text' => json_encode(market_management_mcp_call_idempotent($pdo, $userSubject, $tool, $arguments), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
|
||||
]],
|
||||
'isError' => false,
|
||||
];
|
||||
if (array_key_exists('arguments', $params) && !is_array($params['arguments'])) {
|
||||
throw new MarketManagementMcpJsonRpcException(-32602, 'Tool-Argumente müssen als Objekt übergeben werden.');
|
||||
}
|
||||
try {
|
||||
$result = [
|
||||
'content' => [[
|
||||
'type' => 'text',
|
||||
'text' => json_encode(market_management_mcp_call_idempotent($pdo, $userSubject, $tool, $arguments), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
|
||||
]],
|
||||
'isError' => false,
|
||||
];
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new MarketManagementMcpJsonRpcException(-32602, $e->getMessage(), $e);
|
||||
}
|
||||
} else {
|
||||
throw new InvalidArgumentException('Unbekannte JSON-RPC-Methode: ' . $method);
|
||||
throw new MarketManagementMcpJsonRpcException(-32601, 'Unbekannte JSON-RPC-Methode: ' . $method);
|
||||
}
|
||||
|
||||
echo json_encode(['jsonrpc' => '2.0', 'id' => $id, 'result' => $result], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code($e instanceof RuntimeException && str_contains($e->getMessage(), 'Token') ? 401 : 400);
|
||||
$isAuthError = $e instanceof RuntimeException && str_contains($e->getMessage(), 'Token');
|
||||
$isJsonRpcError = $e instanceof MarketManagementMcpJsonRpcException;
|
||||
http_response_code($isJsonRpcError ? 200 : ($isAuthError ? 401 : 400));
|
||||
echo json_encode([
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => $request['id'] ?? null,
|
||||
'error' => ['code' => -32600, 'message' => $e->getMessage()],
|
||||
'error' => [
|
||||
'code' => $isJsonRpcError ? $e->rpcCode() : -32600,
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
--color-process-inactive: #FFAE79;
|
||||
--color-signal-green: #009101;
|
||||
--color-signal-green-light: #65A665;
|
||||
--color-signal-green-red: #C0624D;
|
||||
--color-signal-yellow: #9C7A00;
|
||||
--color-ampel-hellgelb: #D3B962;
|
||||
--color-signal-red: #9B3B2F;
|
||||
@@ -37,6 +38,7 @@
|
||||
--surface-card-header-muted: var(--color-darkbrown);
|
||||
--divider-card-segment: var(--color-white);
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-signal: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-dark);
|
||||
--text-card-transparent: var(--color-font-light);
|
||||
--layout-card-body-content-justify: flex-start;
|
||||
@@ -144,7 +146,6 @@
|
||||
--layout-preview-align-items: flex-start;
|
||||
|
||||
/* Semantic pattern tokens: Portal header */
|
||||
--surface-portal-background: var(--color-background-purple);
|
||||
--surface-portal-header: var(--color-darkblue);
|
||||
--surface-portal-header-tab: var(--color-midblue);
|
||||
--surface-portal-header-tab-active: var(--color-light-grey);
|
||||
@@ -310,7 +311,6 @@
|
||||
--dimension-input-label-width: 9rem;
|
||||
--dimension-input-field-desktop-width: 400px;
|
||||
--dimension-input-field-max-width: 600px;
|
||||
--dimension-email-body-max-width: 600px;
|
||||
--dimension-search-field-width: 15.3rem;
|
||||
--dimension-public-content-width: 100%;
|
||||
--dimension-layer-pulldown-panel: 40;
|
||||
@@ -358,7 +358,7 @@ body {
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-base);
|
||||
color: var(--color-font-dark);
|
||||
background: var(--surface-portal-background);
|
||||
background: var(--color-background-purple);
|
||||
margin: 0;
|
||||
padding: var(--spacing-small);
|
||||
}
|
||||
@@ -1765,9 +1765,7 @@ section + section {
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel {
|
||||
@@ -1853,6 +1851,47 @@ section + section {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"] {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="bottom"]::before {
|
||||
top: calc(var(--spacing-small) * -1);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent var(--surface-tooltip);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="top"]::before {
|
||||
bottom: calc(var(--spacing-small) * -1);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: var(--surface-tooltip) transparent transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="left"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
right: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent transparent var(--surface-tooltip);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="right"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
left: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-pulldown-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + var(--spacing-small));
|
||||
@@ -2341,6 +2380,52 @@ section + section {
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Pattern: History Graph */
|
||||
/* ========================================================= */
|
||||
|
||||
.sg-history-graph {
|
||||
--layout-history-graph-connector-width: var(--score-marker-width);
|
||||
--surface-history-graph-connector: var(--chart-marker-line);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sg-history-graph__event {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__event:not(:last-child)::after {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
width: var(--layout-history-graph-connector-width);
|
||||
height: var(--spacing-large);
|
||||
background: var(--surface-history-graph-connector);
|
||||
content: "";
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-history-graph__card {
|
||||
width: 100%;
|
||||
max-width: var(--layout-object-card-min-width);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.sg-history-graph__navigation {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Patterns: Portal Header */
|
||||
/* ========================================================= */
|
||||
@@ -2542,19 +2627,39 @@ section + section {
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left {
|
||||
order: 0;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-pulldown-demo,
|
||||
.sg-options-row__left > .sg-input-single-line-wrap,
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 1 1 calc(50% - var(--layout-options-row-group-gap));
|
||||
.sg-options-row__left > .sg-pulldown-demo {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left:has(> .sg-pulldown-demo ~ .sg-pulldown-demo) > .sg-pulldown-demo {
|
||||
flex-basis: calc(50% - (var(--layout-options-row-group-gap) / 2));
|
||||
}
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left > .sg-pulldown-demo {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 0 0 auto;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row > .sg-search-field-input {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row .sg-input-single-line {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-input-single-line-wrap .sg-input-single-line,
|
||||
@@ -4308,22 +4413,27 @@ main {
|
||||
|
||||
.sg-card-segment--signal-red {
|
||||
background: var(--color-signal-red);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green {
|
||||
background: var(--color-signal-green);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-light {
|
||||
background: var(--color-signal-green-light);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-red {
|
||||
background: var(--color-signal-green-red);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-yellow {
|
||||
background: var(--color-signal-yellow);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--gray {
|
||||
@@ -4343,6 +4453,11 @@ main {
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card.sg-card--notification-signal-green-red {
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card--content-card {
|
||||
--surface-card-header-primary: var(--surface-content-block-card-title);
|
||||
--surface-card-body: var(--surface-content-block-card-content);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"styleguideVersion": "2026.05.18.1",
|
||||
"styleguideCommit": "944d791",
|
||||
"syncedAtUtc": "2026-09-21T12:42:55Z",
|
||||
"styleguideVersion": "2026.05.18.2",
|
||||
"styleguideCommit": "519ed6b",
|
||||
"syncedAtUtc": "2026-09-24T11:09:21Z",
|
||||
"sourceRepo": "/Users/mathias/Documents/Dokumente Chouchou/Codebases/Styleguide",
|
||||
"mirroredDocsPath": "docs/styleguide"
|
||||
}
|
||||
+130
-15
@@ -16,6 +16,7 @@
|
||||
--color-process-inactive: #FFAE79;
|
||||
--color-signal-green: #009101;
|
||||
--color-signal-green-light: #65A665;
|
||||
--color-signal-green-red: #C0624D;
|
||||
--color-signal-yellow: #9C7A00;
|
||||
--color-ampel-hellgelb: #D3B962;
|
||||
--color-signal-red: #9B3B2F;
|
||||
@@ -37,6 +38,7 @@
|
||||
--surface-card-header-muted: var(--color-darkbrown);
|
||||
--divider-card-segment: var(--color-white);
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-signal: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-dark);
|
||||
--text-card-transparent: var(--color-font-light);
|
||||
--layout-card-body-content-justify: flex-start;
|
||||
@@ -144,7 +146,6 @@
|
||||
--layout-preview-align-items: flex-start;
|
||||
|
||||
/* Semantic pattern tokens: Portal header */
|
||||
--surface-portal-background: var(--color-background-purple);
|
||||
--surface-portal-header: var(--color-darkblue);
|
||||
--surface-portal-header-tab: var(--color-midblue);
|
||||
--surface-portal-header-tab-active: var(--color-light-grey);
|
||||
@@ -310,7 +311,6 @@
|
||||
--dimension-input-label-width: 9rem;
|
||||
--dimension-input-field-desktop-width: 400px;
|
||||
--dimension-input-field-max-width: 600px;
|
||||
--dimension-email-body-max-width: 600px;
|
||||
--dimension-search-field-width: 15.3rem;
|
||||
--dimension-public-content-width: 100%;
|
||||
--dimension-layer-pulldown-panel: 40;
|
||||
@@ -358,7 +358,7 @@ body {
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-base);
|
||||
color: var(--color-font-dark);
|
||||
background: var(--surface-portal-background);
|
||||
background: var(--color-background-purple);
|
||||
margin: 0;
|
||||
padding: var(--spacing-small);
|
||||
}
|
||||
@@ -1765,9 +1765,7 @@ section + section {
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel {
|
||||
@@ -1853,6 +1851,47 @@ section + section {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"] {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="bottom"]::before {
|
||||
top: calc(var(--spacing-small) * -1);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent var(--surface-tooltip);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="top"]::before {
|
||||
bottom: calc(var(--spacing-small) * -1);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: var(--surface-tooltip) transparent transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="left"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
right: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent transparent var(--surface-tooltip);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="right"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
left: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-pulldown-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + var(--spacing-small));
|
||||
@@ -2341,6 +2380,52 @@ section + section {
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Pattern: History Graph */
|
||||
/* ========================================================= */
|
||||
|
||||
.sg-history-graph {
|
||||
--layout-history-graph-connector-width: var(--score-marker-width);
|
||||
--surface-history-graph-connector: var(--chart-marker-line);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sg-history-graph__event {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__event:not(:last-child)::after {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
width: var(--layout-history-graph-connector-width);
|
||||
height: var(--spacing-large);
|
||||
background: var(--surface-history-graph-connector);
|
||||
content: "";
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-history-graph__card {
|
||||
width: 100%;
|
||||
max-width: var(--layout-object-card-min-width);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.sg-history-graph__navigation {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Patterns: Portal Header */
|
||||
/* ========================================================= */
|
||||
@@ -2542,19 +2627,39 @@ section + section {
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left {
|
||||
order: 0;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-pulldown-demo,
|
||||
.sg-options-row__left > .sg-input-single-line-wrap,
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 1 1 calc(50% - var(--layout-options-row-group-gap));
|
||||
.sg-options-row__left > .sg-pulldown-demo {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left:has(> .sg-pulldown-demo ~ .sg-pulldown-demo) > .sg-pulldown-demo {
|
||||
flex-basis: calc(50% - (var(--layout-options-row-group-gap) / 2));
|
||||
}
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left > .sg-pulldown-demo {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 0 0 auto;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row > .sg-search-field-input {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row .sg-input-single-line {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-input-single-line-wrap .sg-input-single-line,
|
||||
@@ -4308,22 +4413,27 @@ main {
|
||||
|
||||
.sg-card-segment--signal-red {
|
||||
background: var(--color-signal-red);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green {
|
||||
background: var(--color-signal-green);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-light {
|
||||
background: var(--color-signal-green-light);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-red {
|
||||
background: var(--color-signal-green-red);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-yellow {
|
||||
background: var(--color-signal-yellow);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--gray {
|
||||
@@ -4343,6 +4453,11 @@ main {
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card.sg-card--notification-signal-green-red {
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card--content-card {
|
||||
--surface-card-header-primary: var(--surface-content-block-card-title);
|
||||
--surface-card-body: var(--surface-content-block-card-content);
|
||||
|
||||
@@ -52,6 +52,19 @@ body.sg-card-list-page-drawer-open {
|
||||
background: var(--color-background-purple);
|
||||
}
|
||||
|
||||
.sg-market-definition-mobile-detail-host {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1500;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
padding: var(--spacing-small);
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
background: var(--color-background-purple);
|
||||
}
|
||||
|
||||
.sg-market-mobile-detail-host .sg-card-list-page-drawer {
|
||||
position: static;
|
||||
display: flex;
|
||||
@@ -62,10 +75,24 @@ body.sg-card-list-page-drawer-open {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.sg-market-definition-mobile-detail-host .sg-card-list-page-drawer {
|
||||
position: static;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
body.sg-market-mobile-detail-open > :not([data-market-mobile-detail-host]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.sg-market-definition-mobile-detail-open > :not([data-market-definition-mobile-detail-host]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.market-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(10rem, 0.8fr) minmax(0, 1.2fr);
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const create = ({ contentRoot, bodyClass, hostClass, hostDataAttribute, drawerDataAttribute, backdropSelector, backMarkup, historyKey }) => {
|
||||
let host = null;
|
||||
let drawer = null;
|
||||
let trigger = null;
|
||||
let open = false;
|
||||
|
||||
const restore = () => {
|
||||
if (host && host.parentNode) {
|
||||
const mobileDrawer = host.querySelector('[' + drawerDataAttribute + ']');
|
||||
if (mobileDrawer) {
|
||||
contentRoot.appendChild(mobileDrawer);
|
||||
mobileDrawer.removeAttribute(drawerDataAttribute);
|
||||
mobileDrawer.dataset.open = 'false';
|
||||
mobileDrawer.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
host.parentNode.removeChild(host);
|
||||
}
|
||||
|
||||
if (trigger) {
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
const backdrops = backdropSelector
|
||||
? [contentRoot.querySelector(backdropSelector)].filter(Boolean)
|
||||
: Array.from(contentRoot.querySelectorAll('.sg-card-list-page-drawer-backdrop'));
|
||||
backdrops.forEach((backdrop) => {
|
||||
backdrop.dataset.open = 'false';
|
||||
backdrop.setAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
host = null;
|
||||
drawer = null;
|
||||
trigger = null;
|
||||
open = false;
|
||||
document.body.classList.remove(bodyClass);
|
||||
document.body.classList.remove('sg-card-list-page-drawer-open');
|
||||
};
|
||||
|
||||
const mount = (sourceDrawer, detailTrigger, existingHost = null) => {
|
||||
restore();
|
||||
if (!sourceDrawer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
host = existingHost || document.createElement('div');
|
||||
host.className = hostClass;
|
||||
host.setAttribute(hostDataAttribute, 'true');
|
||||
if (!existingHost) {
|
||||
host.innerHTML = backMarkup;
|
||||
}
|
||||
drawer = sourceDrawer;
|
||||
trigger = detailTrigger;
|
||||
drawer.setAttribute(drawerDataAttribute, 'true');
|
||||
drawer.dataset.open = 'true';
|
||||
drawer.setAttribute('aria-hidden', 'false');
|
||||
host.appendChild(drawer);
|
||||
document.body.appendChild(host);
|
||||
document.body.classList.add(bodyClass);
|
||||
open = true;
|
||||
|
||||
if (historyKey) {
|
||||
window.history.pushState({ ...(window.history.state || {}), [historyKey]: 'detail' }, '', window.location.href);
|
||||
}
|
||||
|
||||
return { host, drawer, trigger: detailTrigger };
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
if (window.history.state && window.history.state[historyKey] === 'detail') {
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
restore();
|
||||
};
|
||||
|
||||
return { mount, restore, close, isOpen: () => open, getHost: () => host, getDrawer: () => drawer };
|
||||
};
|
||||
|
||||
window.SharedMobileDetail = { create };
|
||||
}());
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../modules/market_management/mcp.php';
|
||||
|
||||
function fail_test(string $message): void
|
||||
{
|
||||
fwrite(STDERR, $message . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function assert_test(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
fail_test($message);
|
||||
}
|
||||
}
|
||||
|
||||
function assert_schema_objects(array $schema, string $path = 'inputSchema'): void
|
||||
{
|
||||
assert_test(($schema['type'] ?? null) === 'object', $path . '.type must be object');
|
||||
assert_test(isset($schema['properties']) && is_object($schema['properties']), $path . '.properties must be an object');
|
||||
|
||||
if (isset($schema['required'])) {
|
||||
assert_test(is_array($schema['required']), $path . '.required must be an array');
|
||||
foreach ($schema['required'] as $required) {
|
||||
assert_test(is_string($required), $path . '.required entries must be strings');
|
||||
}
|
||||
}
|
||||
|
||||
foreach (get_object_vars($schema['properties']) as $propertyName => $propertySchema) {
|
||||
assert_test(is_array($propertySchema), $path . '.properties.' . $propertyName . ' must be an object schema');
|
||||
if (($propertySchema['type'] ?? null) === 'array' && isset($propertySchema['items'])) {
|
||||
assert_test(is_array($propertySchema['items']), $path . '.properties.' . $propertyName . '.items must be an object schema');
|
||||
if (($propertySchema['items']['type'] ?? null) === 'object') {
|
||||
assert_schema_objects($propertySchema['items'], $path . '.properties.' . $propertyName . '.items');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$expected = [
|
||||
'market_management.search_markets' => 'Märkte suchen',
|
||||
'market_management.search_occurrences' => 'Marktdurchführungen suchen und auflisten',
|
||||
'market_management.get_market' => 'Markt mit Marktdurchführungen lesen',
|
||||
'market_management.create_market' => 'Markt mit verpflichtendem Veranstalter, Kategorie und optionaler Website-URL anlegen',
|
||||
'market_management.update_market' => 'Markt inklusive Veranstalter und Website-URL ändern',
|
||||
'market_management.archive_market' => 'Markt archivieren',
|
||||
'market_management.reactivate_market' => 'Markt reaktivieren',
|
||||
'market_management.list_categories' => 'Bestehende Kategorien lesen',
|
||||
'market_management.list_market_organizers' => 'Aktive Veranstalter lesen',
|
||||
'market_management.create_market_organizer' => 'Veranstalter mit optionalen Kontaktdaten anlegen',
|
||||
'market_management.list_reference_data' => 'Kategorien, Kantone und Gemeinden lesen',
|
||||
'market_management.create_municipality' => 'Lokale Gemeinde im Markt-Gemeinderegister anlegen',
|
||||
'market_management.get_overview' => 'Marktübersicht mit serverseitigen Filtern lesen',
|
||||
'market_management.create_occurrence' => 'Marktdurchführung mit mindestens einem Markttag anlegen',
|
||||
'market_management.get_occurrence' => 'Marktdurchführung mit Markt, Markttagen und berechneten Verkaufsdaten lesen',
|
||||
'market_management.update_occurrence' => 'Marktdurchführung ändern',
|
||||
'market_management.archive_occurrence' => 'Marktdurchführung archivieren',
|
||||
'market_management.reactivate_occurrence' => 'Marktdurchführung reaktivieren',
|
||||
'market_management.create_day' => 'Markttag anlegen',
|
||||
'market_management.update_day' => 'Markttag ändern',
|
||||
'market_management.delete_day' => 'Markttag entfernen',
|
||||
];
|
||||
|
||||
$tools = market_management_mcp_tools();
|
||||
assert_test(count($tools) === 21, 'Expected 21 MCP tools, got ' . count($tools));
|
||||
|
||||
$actual = [];
|
||||
foreach ($tools as $tool) {
|
||||
assert_test(isset($tool['name'], $tool['description'], $tool['inputSchema']), 'Tool must contain name, description and inputSchema');
|
||||
assert_schema_objects($tool['inputSchema'], $tool['name'] . '.inputSchema');
|
||||
$actual[$tool['name']] = $tool['description'];
|
||||
}
|
||||
|
||||
assert_test($actual === $expected, 'MCP tool names or descriptions changed unexpectedly');
|
||||
fwrite(STDOUT, "market_management_mcp_tools_list_test: OK\n");
|
||||
Reference in new issue
Block a user