Compare commits

...
Author SHA1 Message Date
gitea_admin 8fa9714793 Initialisiere Checkliste vor Markttagen 2026-10-18 09:52:33 +02:00
gitea_admin d884793cb0 Expose MCP idempotency keys 2026-10-18 09:41:53 +02:00
gitea_admin 937d772c08 Fix market travel boolean serialization 2026-10-17 07:45:39 +02:00
gitea_admin a82515673d Sync styleguide 2026.05.18.2 2026-10-14 13:46:02 +02:00
gitea_admin c7ab2f2817 Sync styleguide 2026.05.18.2 2026-10-14 13:33:42 +02:00
gitea_admin b8ba2db7a5 Fix occurrence MCP schema and expose product sales 2026-10-12 21:47:12 +02:00
gitea_admin d871b94c52 OTC-Betragsfeld leer initialisieren 2026-10-01 08:39:26 +02:00
gitea_admin 4a819f7295 Marktbezug im Bestellungsdrawer anzeigen 2026-09-30 20:11:15 +02:00
gitea_admin 2ae368d2d6 OTC-Verkauf aktualisiert Bestellungen-Tabelle sofort 2026-09-30 20:07:00 +02:00
gitea_admin 9f35366449 Prevent checklist checkbox shrinking 2026-09-30 12:15:54 +02:00
gitea_admin dea0cd9e39 Centralize editable card visibility toggle 2026-09-30 12:13:01 +02:00
gitea_admin cc534b0ce4 Add fixed chapters to market occurrence checklists 2026-09-30 11:59:35 +02:00
gitea_admin 84a4e6ac63 Fix ERP lager MCP schema properties 2026-09-30 11:07:44 +02:00
gitea_admin 1b7e0c8a13 Add separate read-only ERP lager MCP 2026-09-30 10:51:03 +02:00
gitea_admin b0bf8753ed Disable checklist toggle while editing 2026-09-30 10:43:19 +02:00
gitea_admin 739ef662b9 Keep checklist checks beside inputs on mobile 2026-09-30 10:41:05 +02:00
gitea_admin d0aa29107d Keep checklist visible on mobile save 2026-09-30 10:40:17 +02:00
gitea_admin e25899b3ee Delay scroll restoration after card refresh 2026-09-30 10:37:38 +02:00
gitea_admin 01fa2bc2b6 Preserve card edit scroll position 2026-09-30 10:36:09 +02:00
gitea_admin ec6364b32b Sync checklist edit checks 2026-09-30 10:32:25 +02:00
gitea_admin cd3bd1e1b1 Fix checklist signal refresh 2026-09-30 10:31:03 +02:00
gitea_admin 332a385ccf Color checklist status header 2026-09-30 10:28:38 +02:00
gitea_admin 5935144a5a Toggle checklist visibility 2026-09-30 10:20:55 +02:00
gitea_admin 1de403f39d Align checklist edit fields 2026-09-30 10:15:57 +02:00
gitea_admin f9b5383ae9 Add occurrence checklist submodule 2026-09-30 10:08:48 +02:00
gitea_admin 5dbbac35c3 Sync styleguide 2026.05.18.2 2026-09-28 11:06:53 +02:00
gitea_admin 414781db74 Sync styleguide 2026.05.18.2 2026-09-28 10:50:35 +02:00
gitea_admin 46e31c5cbd Märkte-Tabelle vollständig sortierbar machen 2026-09-28 10:46:19 +02:00
gitea_admin 12a376c8fa Cachebusting für zentrale Tabellen-CSS ergänzen 2026-09-28 10:40:40 +02:00
gitea_admin 46a5eb53f1 Sync styleguide 2026.05.18.2 2026-09-28 10:36:07 +02:00
gitea_admin 07a48bbc9c Sync styleguide 2026.05.18.2 2026-09-28 10:34:47 +02:00
gitea_admin 3aaa188d38 Sync styleguide 2026.05.18.2 2026-09-28 10:33:12 +02:00
gitea_admin 1286333ead Sync styleguide 2026.05.18.2 2026-09-28 10:31:34 +02:00
gitea_admin 6a77b56d34 Sync styleguide 2026.05.18.2 2026-09-28 09:04:15 +02:00
gitea_admin a6cfcd4ccd Stelle horizontalen Tabellenscroll wieder her 2026-09-28 08:24:38 +02:00
gitea_admin 80f81bd217 Korrigiere Mehr-laden-Card in Tabellen 2026-09-28 08:21:58 +02:00
gitea_admin ccbcde70a6 Entferne Standardbreiten aus Tabellen 2026-09-28 07:58:15 +02:00
gitea_admin f346fc68e0 Sync styleguide 2026.05.18.2 2026-09-28 07:56:50 +02:00
gitea_admin 7306ded7dd Sync styleguide 2026.05.18.2 2026-09-28 07:43:52 +02:00
gitea_admin ae741aae61 Website-URLs ohne Protokoll akzeptieren 2026-09-27 13:48:29 +02:00
gitea_admin b963b798df Status-Card unter Marktdurchfuehrung verschieben 2026-09-27 09:20:24 +02:00
gitea_admin de2c6d0755 Anmeldehinweis sanitizen 2026-09-27 09:17:03 +02:00
gitea_admin 7f61df4052 Anmeldehinweis fuer Marktdurchfuehrungen ergaenzen 2026-09-27 09:14:33 +02:00
gitea_admin 367af9a1a7 Dokumentationspfad fuer MCP festhalten 2026-09-27 09:09:57 +02:00
gitea_admin 23e1c6f9d1 Fix MCP schema test 2026-09-27 08:50:20 +02:00
gitea_admin 345b015137 Remove market travel duration 2026-09-27 08:43:12 +02:00
gitea_admin 3c12bace28 Prevent large table resize observer loop 2026-09-27 08:33:12 +02:00
gitea_admin 817d3c10d1 Enable resizable columns for all ERP large tables 2026-09-27 08:29:26 +02:00
gitea_admin 900e7dee1c Sync styleguide 2026.05.18.2 2026-09-27 08:25:30 +02:00
gitea_admin ca6015024b Sync styleguide 2026.05.18.2 2026-09-27 08:20:05 +02:00
gitea_admin 8733ce6148 Fix MCP occurrence apostrophe escaping 2026-09-27 07:49:37 +02:00
gitea_admin 85834a0498 Sync styleguide 2026.05.18.2 2026-09-25 13:10:50 +02:00
gitea_admin 732068ad9c Unify mobile detail navigation 2026-09-25 08:19:56 +02:00
gitea_admin ea434033f0 Align MCP JSON-RPC error responses 2026-09-24 14:56:31 +02:00
gitea_admin ebb71b4769 Fix MCP tools/list schema serialization 2026-09-24 14:51:52 +02:00
gitea_admin 8c5668bf3d Sync styleguide 2026.05.18.2 2026-09-24 13:09:21 +02:00
gitea_admin 53baf0dfbf Sync styleguide 2026.05.18.2 2026-09-24 13:00:00 +02:00
gitea_admin 0c04caeb67 Sync styleguide 2026.05.18.2 2026-09-22 11:50:21 +02:00
gitea_admin 8b163c5734 Sync styleguide 2026.05.18.1 2026-09-22 09:55:02 +02:00
gitea_admin a6007b5853 Sync styleguide 2026.05.18.1 2026-09-21 17:27:45 +02:00
gitea_admin 386b539f88 Sync styleguide 2026.05.18.1 2026-09-21 16:24:42 +02:00
gitea_admin 58c5912c30 Sync styleguide 2026.05.18.1 2026-09-21 16:06:04 +02:00
gitea_admin 17e07d525b Require raw HTML from Hermes MCP 2026-09-21 15:50:19 +02:00
gitea_admin dd019afff6 Normalize escaped Hermes rich text 2026-09-21 15:45:53 +02:00
gitea_admin 276a496ae8 Fix market occurrence drawer rendering 2026-09-21 15:43:21 +02:00
gitea_admin 9f10344fa7 Allow external web links in market rich text 2026-09-21 15:33:38 +02:00
gitea_admin c25d3f1b3c Allow safe rich text in market multiline fields 2026-09-21 15:30:13 +02:00
gitea_admin 73e2144571 Merge market document links 2026-09-21 15:03:29 +02:00
59 changed files with 3428 additions and 831 deletions

No files matched your search

+7
View File
@@ -53,3 +53,10 @@ Diese Datei enthaelt operative Arbeitsregeln fuer Agenten in diesem Repository.
- Wenn Prozesse umgesetzt werden: verwende immer den Skill prozess-umsetzung
- Wenn UI umgesetzt werden soll: verwende immer den Skill styleguide-anwendung
- Zwingende Vorgabe für alle Arbeiten ist /docs/technical_architecture.md
## 9. MCP-Dokumentation
- Bei jeder Änderung an der MCP-Schnittstelle des Marktmanagements ist neben der
Repository-Dokumentation auch die MCP-Dokumentation unter
`/Volumes/Hermes/Hermes Naurua/Marktmanagement/MCP` zu aktualisieren.
- Die MCP-Dokumentation ist damit ein verbindlicher zusätzlicher Dokumentationsstand
für alle zukünftigen MCP-Änderungen in diesem Projekt.
+2
View File
@@ -20,6 +20,8 @@
16. `db/migrations/0020_include_stock_adjustments_in_balance.sql`
17. `db/migrations/0021_stock_lot_activation_selection.sql`
18. `db/migrations/0022_backfill_stock_lot_activation_selection.sql`
19. `db/migrations/0023_market_management.sql` bis `db/migrations/0034_market_occurrence_checklist.sql`
20. `db/migrations/0035_market_occurrence_checklist_chapters.sql`
## Beispielausfuehrung
@@ -0,0 +1,8 @@
BEGIN;
-- Reisedauer wird fachlich nicht mehr verwendet und vollständig entfernt.
ALTER TABLE market_occurrence
DROP CONSTRAINT IF EXISTS chk_market_occurrence_travel_duration,
DROP COLUMN IF EXISTS travel_duration_minutes;
COMMIT;
@@ -0,0 +1,2 @@
ALTER TABLE market_occurrence
ADD COLUMN registration_note TEXT;
@@ -0,0 +1,33 @@
BEGIN;
CREATE TABLE market_occurrence_checklist (
id BIGSERIAL PRIMARY KEY,
occurrence_id BIGINT NOT NULL UNIQUE REFERENCES market_occurrence(id) ON DELETE RESTRICT,
comment TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE market_occurrence_checklist_item (
id BIGSERIAL PRIMARY KEY,
occurrence_id BIGINT NOT NULL REFERENCES market_occurrence(id) ON DELETE RESTRICT,
position INTEGER NOT NULL,
label TEXT NOT NULL,
is_completed BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT chk_market_occurrence_checklist_item_position CHECK (position > 0)
);
CREATE INDEX idx_market_occurrence_checklist_item_occurrence
ON market_occurrence_checklist_item(occurrence_id, position, id);
INSERT INTO market_occurrence_checklist (occurrence_id)
SELECT id FROM market_occurrence;
INSERT INTO market_occurrence_checklist_item (occurrence_id, position, label)
SELECT occurrence_id, position, 'Position ' || position
FROM market_occurrence_checklist
CROSS JOIN generate_series(1, 30) AS positions(position);
COMMIT;
@@ -0,0 +1,53 @@
BEGIN;
ALTER TABLE market_occurrence_checklist_item
ADD COLUMN chapter_key TEXT;
DELETE FROM market_occurrence_checklist_item;
UPDATE market_occurrence_checklist
SET comment = NULL,
updated_at = NOW();
ALTER TABLE market_occurrence_checklist_item
ALTER COLUMN chapter_key SET NOT NULL,
ADD CONSTRAINT chk_market_occurrence_checklist_item_chapter
CHECK (chapter_key IN ('products_sales', 'decoration', 'stand_equipment', 'technology'));
INSERT INTO market_occurrence_checklist_item
(occurrence_id, chapter_key, position, label, is_completed)
SELECT checklist.occurrence_id,
defaults.chapter_key,
defaults.position,
defaults.label,
FALSE
FROM market_occurrence_checklist checklist
CROSS JOIN (
VALUES
('products_sales', 1, 'Fläschchen'),
('products_sales', 2, 'Säckchen'),
('products_sales', 3, 'Dunkle Kleber'),
('products_sales', 4, 'Naurua-Kleber'),
('products_sales', 5, 'Anwendungsbeilage'),
('products_sales', 6, 'Flyer Naurua'),
('products_sales', 7, 'Flyer Vitalpilze'),
('products_sales', 8, 'Vitalpilzbuch'),
('products_sales', 9, 'SumUp-Terminal'),
('products_sales', 10, 'Bargeld'),
('decoration', 1, 'Dekoration'),
('decoration', 2, 'Pilze'),
('decoration', 3, 'Blumenvase'),
('decoration', 4, 'Blume'),
('stand_equipment', 1, 'Tisch'),
('stand_equipment', 2, 'Tischtücher'),
('stand_equipment', 3, 'Beleuchtung'),
('stand_equipment', 4, 'Holzklötze'),
('stand_equipment', 5, 'Wagen'),
('stand_equipment', 6, 'Sonnenschirm'),
('stand_equipment', 7, 'Parkkarte/-Marktinformationen'),
('technology', 1, 'Batterie-/Powerbank fürs Handy')
) AS defaults(chapter_key, position, label);
CREATE INDEX idx_market_occurrence_checklist_item_chapter
ON market_occurrence_checklist_item(occurrence_id, chapter_key, position, id);
COMMIT;
+3
View File
@@ -42,6 +42,9 @@ Bestand, Charge, MHD und Bewegungen.
## Technisch umgesetzt
- `modules/erp/lager/service.php`
- Read-only MCP für Hermes: `modules/erp/lager/mcp.php`
- MCP-Endpoint: `public/api/erp-lager-mcp.php`
- [ERP-Lager-Hermes-MCP-API](konzepte/erp_lager_hermes_mcp_api.md)
Hersteller-Stammdaten gehören zum Modul `kontakte`. Eine reale Lagercharge kann dort einen Hersteller und dessen Hersteller-Chargennummer referenzieren; die interne Naurua-Chargennummer bleibt Eigentum des Moduls `lager`. Virtuelle Fehlbestandschargen haben keinen Herstellerbezug.
@@ -0,0 +1,31 @@
# Hermes: ERP-Lager-MCP-API
## Zweck
Das ERP-Lager stellt ein eigenes, read-only MCP-Interface bereit. Hermes kann dadurch ausschließlich die Lager-Tools laden, ohne Marktmanagement-Tools in seinen Kontext aufzunehmen.
Endpoint: `https://erpnaurua.imhochrain.ch/api/erp-lager-mcp.php`
Authentifizierung: `Authorization: Bearer <MARKET_MCP_TOKEN>`
Der Token ist identisch mit dem Marktmanagement-MCP, weil derselbe Hermes-Bot auf beide fachlich getrennten Interfaces zugreift. Die Toolsets und Endpoints bleiben trotzdem getrennt.
## Verfügbare Tools
### `erp_lager.list_products`
Liefert alle aktiven Produkte mit ihren stabilen IDs.
### `erp_lager.get_product`
Liest alle Lagerdaten eines aktiven Produkts. Eingabe: `{ "product_id": 12 }`.
Die Antwort enthält die GUI-parallele Produktübersicht mit Produktstammdaten, aktueller und verfügbarer Lagermenge, Menge im Wareneingang, aktueller Charge, Ausverkaufsdatum und Forecast-Status, nächster aktiver offener Charge, weiteren offenen zukünftigen Chargen, historischen geschlossenen Chargen sowie virtueller Fehlbestandsmenge und virtueller Fehlbestandscharge.
Das Interface ist ausschließlich lesend. Es gibt keine Lager-MCP-Tools zum Anlegen, Ändern, Buchen, Verschieben oder Löschen.
## JSON-RPC
Das Interface unterstützt `initialize`, `notifications/initialized`, `tools/list` und `tools/call` gemäß dem bestehenden Marktmanagement-MCP-Transport.
Bei `tools/list` ist `inputSchema.properties` für jedes Werkzeug immer ein JSON-Objekt; beim parameterlosen `erp_lager.list_products` ist es `{}`.
@@ -13,6 +13,8 @@ Status: Verbindliches DB-Ownership-Konzept
| `market_occurrence_organizer` | `market_management` | Marktmanagement-Service über Kontakt-Schnittstelle |
| `market_occurrence_day` | `market_management` | Marktmanagement-Service |
| `market_occurrence_cost_item` | `market_management` | Marktmanagement-Service |
| `market_occurrence_checklist` | `market_management` | Submodul `occurrence_checklist` |
| `market_occurrence_checklist_item` | `market_management` | Submodul `occurrence_checklist` |
| `market_municipality_reference` | `market_management` | Referenzdatenprozess |
| `market_municipality` | `market_management` | Marktmanagement-Service |
| `market_mcp_idempotency` | `market_management` | MCP-Submodul |
@@ -1,8 +1,11 @@
# Hermes: Market-Management-MCP-API
Stand: 2026-09-20
Stand: 2026-09-27
Status: Aktive technische Schnittstellendokumentation
Die Marktdurchführungs-Schnittstelle enthält kein Feld `travel_duration_minutes`.
Die Reisedauer wird nicht gespeichert, gelesen, geändert oder ausgegeben.
## Verbindung
Hermes verwendet den MCP-Endpunkt der DEV-App:
@@ -25,14 +28,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 +44,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 `&lt;`, `&gt;` oder
`&quot;`. 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
@@ -128,12 +138,12 @@ Das Ergebnis steht bei `tools/call` als JSON im Textinhalt des ersten
- liefert Marktdurchführungen inklusive Markttagen, Kosten und berechneten Verkaufswerten
- `market_management.create_market`
- erforderlich: `name`, `category_id`, `organizer_id`
- optional: `website_url` als HTTP(S)-URL des Marktes; Eingaben ohne Schema werden als `https://` normalisiert
- `market_management.list_market_organizers`
- liefert aktive Veranstalterorganisationen
- `market_management.create_market_organizer`
- erforderlich: `name`
- optional: `email`, `phone`
- optional: `website_url` als HTTP(S)-URL des Marktes
- `market_management.update_market`
- erforderlich: `market_id`
- änderbar: `name`, `category_id`, `website_url`
@@ -163,15 +173,32 @@ Kategorien anlegen oder ändern.
- erforderlich: `market_definition_id`, `days` mit mindestens einem Markttag
- initialer Status: `open`
- weitere Marktdurchführungsfelder sind optional und werden typisiert validiert
- legt automatisch eine Standard-Checkliste mit den vier festen Kapiteln an
- `market_management.get_occurrence`
- erforderlich: `occurrence_id`
- liefert zusätzlich den Markt, Markttage, Kosten und
berechnete Tages- und Gesamterlöse
berechnete Tages- und Gesamterlöse sowie `sales_items` je Markttag
- `market_management.get_occurrence_checklist`
- erforderlich: `occurrence_id`
- liefert die vier festen Kapitel, die Punkte mit `chapter_key` und den
Checklistenkommentar
- `market_management.update_occurrence_checklist`
- erforderlich: `occurrence_id`, `items`
- ersetzt die vollständige Checkliste atomar
- `items` enthält `chapter_key`, `label` und `is_completed`; `comment` ist optional und nullable
- feste Kapitel: `products_sales` (Produkte & Verkauf), `decoration` (Dekoration),
`stand_equipment` (Stand & Ausstattung), `technology` (Technik)
- Kapitel sind nicht editierbar; Hermes darf Punkte ergänzen, entfernen,
umbenennen, abhaken und zurücksetzen
- `market_management.update_occurrence`
- erforderlich: `occurrence_id`
- änderbar: sämtliche editierbaren GUI-Marktdurchführungsfelder einschließlich
Gemeinde, Kontaktperson, Kontakt-E-Mail und Kontakt-Telefon
Gemeinde, Kontaktperson, Kontakt-E-Mail, Kontakt-Telefon und `registration_note`
- der vom Markt geerbte Veranstalter ist nicht editierbar
Das optionale Feld `registration_note` speichert den Freitext-Hinweis zur
Anmeldung einer Marktdurchführung. Es wird bei `create_occurrence`,
`get_occurrence` und `update_occurrence` als nullable String verarbeitet.
- `market_management.archive_occurrence`
- erforderlich: `occurrence_id`
- `market_management.reactivate_occurrence`
@@ -297,12 +324,15 @@ Weitere mögliche Tagesfelder sind `stand_start_time`, `stand_end_time`,
- Hermes darf keine Kategorien anlegen.
- Umsatz und Artikelmengen werden nicht über MCP geschrieben; sie werden aus
den OTC-Verkäufen abgeleitet.
- `get_occurrence` liefert `sales_items` mit `day_id`, `date`, `product_id`,
`sku`, `product_name`, `quantity`, `gross_revenue` und `unit_price`.
- Markt und Marktdurchführung werden unabhängig archiviert.
- Alle Schreibvorgänge werden serverseitig validiert.
- Jeder schreibende MCP-Aufruf benötigt `idempotency_key`; optional kann
- Jeder schreibende MCP-Aufruf benötigt `idempotency_key`; das Feld ist in den
Schemas aller Schreibwerkzeuge als Pflichtfeld deklariert. Optional kann
`client_id` mitgegeben werden. Wiederholungen liefern dasselbe Ergebnis.
- Umsatz- und Artikelmengen aus OTC-Verkäufen sind erst verfügbar, sobald die
separate OTC-Schnittstelle die Zuordnung zum Markttag bereitstellt.
- Die gelieferten Verkaufspositionen stammen ausschließlich aus OTC-Verkäufen,
die einem konkreten Markttag zugeordnet sind.
## Fehler
@@ -36,6 +36,7 @@ Vorgesehene Toolgruppen:
- Marktdurchführungen suchen und lesen
- Marktdurchführungen anlegen, ändern, archivieren und reaktivieren
- Markttage anlegen, ändern und entfernen
- Checklisten einer Marktdurchführung lesen und vollständig ändern
- Kategorien, Kantone und Gemeinden lesen
- Kosten und Zahlungsstatus ändern
- Übersichtsdaten lesen
@@ -67,11 +68,17 @@ Modulberechtigung.
- Jeder Tool-Aufruf ist atomar.
- Fehlgeschlagene Aufrufe erzeugen keine Teilpersistierung.
- Alle Schreibvorgänge laufen über die interne Modul-API.
- Checklisten-Schreibvorgänge ersetzen die vollständige Checkliste atomar. Hermes darf Punkte ergänzen, entfernen, umbenennen, abhaken und zurücksetzen sowie den Checklistenkommentar ändern.
- 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
`&lt;`, `&gt;` oder `&quot;` 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.
@@ -13,6 +13,8 @@ Tabellen:
- `market_occurrence_organizer`
- `market_occurrence_day`
- `market_occurrence_cost_item`
- `market_occurrence_checklist`
- `market_occurrence_checklist_item`
- `market_municipality_reference`
- `market_municipality`
- `market_mcp_idempotency`
@@ -31,8 +33,15 @@ Prozessbereiche:
- Umsatz- und Artikelmengen lesen und aggregieren
- Marktübersicht lesen
- Hermes-MCP-Zugriff ausführen
- Marktdurchführungs-Checkliste verwalten
- Referenzdaten verwalten
Die Checkliste gehört zur Marktdurchführung, nicht zum einzelnen Markttag.
Sie besitzt die vier festen Kapitel `Produkte & Verkauf`, `Dekoration`,
`Stand & Ausstattung` und `Technik`. Kapitel und Kapitelreihenfolge sind
fest; Punkte innerhalb eines Kapitels bleiben für Benutzer und Hermes
editierbar. Der Kommentar gehört zur gesamten Checkliste.
## 2. Zweck und Scope
Dieses Dokument beschreibt das normative technische Zielbild für die Umsetzung
@@ -103,6 +112,7 @@ Normativ:
Zahlungsdaten gehören zur Durchführung
- der Status gehört zur Durchführung
- Zahlungsstatus und Marktstatus sind unabhängig
- der optionale `registration_note` gehört als Freitext zur Durchführung
- der Veranstalter wird von jeder Durchführung des Marktes geerbt und ist dort nicht editierbar
- der Zahlungsstatus ist manuell `paid` oder `unpaid`
@@ -13,6 +13,7 @@ Das Pattern kapselt nur die technische Interaktion:
- optionale Loeschbestaetigung mit konfigurierbarem Token
- Deaktivieren der Loeschaktion bis zur gueltigen Bestaetigung
- gleichzeitiges Bearbeiten mehrerer Karten
- optionales Ein- und Ausblenden des Karteninhalts
Fachliche Aktionen, API-Aufrufe, Validierung und die Aktualisierung fachlicher
Daten bleiben beim owning Modul.
@@ -26,6 +27,9 @@ Bedienelemente verwenden folgende Attribute:
- `data-shared-editable-card-cancel`
- `data-shared-editable-card-read`
- `data-shared-editable-card-form`
- optional `data-shared-editable-card-collapsible`
- optional `data-shared-editable-card-collapsed`
- optional `data-shared-editable-card-toggle`
- optional `data-shared-editable-card-delete`
- optional `data-shared-editable-card-delete-confirmation`
- optional `data-shared-editable-card-delete-input`
@@ -41,6 +45,20 @@ Das Loesch-Token wird je Karte mit
Loeschen bleibt im owning Modul und verwendet den bestehenden fachlichen
Handler.
## Optionales Ein- und Ausblenden
Eine Karte mit `data-shared-editable-card-collapsible` kann ihren Inhalt
ein- und ausblenden. Mit `data-shared-editable-card-collapsed` wird der initial
eingeklappte Zustand markiert. Der Toggle verwendet die Beschriftungen
„Anzeigen“ und „Verbergen“. Beim Wechsel in den Bearbeitungsmodus wird der
Inhalt eingeblendet und der Toggle deaktiviert. Nach dem Bearbeiten bleibt der
aktuelle Sichtbarkeitszustand erhalten.
Die zentrale Komponente stellt dafür `setVisibility(card, visible)`,
`isVisible(card)` und `setToggleEnabled(card, enabled)` bereit. Fachmodule
verwenden diese Methoden nur für fachliche Folgeaktionen wie das erneute
Rendern nach dem Speichern.
## Nutzung im Lager
Im Lager-Drawer verwenden die aktive Charge und offene Chargen das Pattern.
@@ -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
View File
@@ -1 +1 @@
2026.05.18.1
2026.05.18.2
+20 -16
View File
@@ -77,8 +77,10 @@
<p class="sg-preview-label">Component: Large Table</p>
<p>Suche, Sortierung und „Mehr laden“ sind hier Demo-Markup. Produktions-Implementierungen müssen Treffer serverseitig neu rendern.</p>
<p>Optionale Spaltenbreiten-Anpassung: <code>data-column-resize="true"</code> aktiviert die Funktion. Die Tabelle benötigt dafür eine stabile, portalweit eindeutige <code>data-column-resize-key</code>. Die Header-Zellen können mit <code>data-default-width</code> (Prozent, Pixel oder relative <code>fr</code>-Werte) und die Tabelle mit <code>data-column-min-width</code> konfiguriert werden. Die Runtime wird nach dynamischem Nachladen über <code>window.sgInitLargeTableColumnResize({ root })</code> erneut aufgerufen. Gespeicherte Werte liegen als <code>{ version: 3, ratios: number[] }</code> unter <code>sg:large-table:column-widths:&lt;key&gt;</code> in <code>localStorage</code>; die Werte sind proportionale Anteile der verfügbaren Tabellenbreite.</p>
<article class="sg-card sg-large-table" data-component="large-table" role="table" aria-label="Beispiel Large Table">
<article id="styleguide-large-table" class="sg-card sg-large-table" data-component="large-table" data-column-resize="true" data-column-resize-key="styleguide-large-table" data-column-min-width="8rem" role="table" aria-label="Beispiel Large Table">
<div class="sg-large-table__viewport" data-component-part="large-table-viewport">
<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header">
<div class="sg-strong">Large Table</div>
<span class="sg-search-field-row" data-component="search-field">
@@ -96,31 +98,35 @@
</div>
<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--header" role="row" data-component-part="large-table-header-row">
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="0">
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="0" data-default-width="20%">
<button class="sg-large-table__sort-button" type="button" aria-label="Spalte 1 sortieren">
<span class="sg-large-table__sort-label">Spalte 1</span>
<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="ascending"></span>
</button>
<span class="sg-large-table__resize-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Breite von Spalte 1 anpassen" data-large-table-resize-handle="0"></span>
</div>
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="1">
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="1" data-default-width="20%">
<button class="sg-large-table__sort-button" type="button" aria-label="Spalte 2 sortieren">
<span class="sg-large-table__sort-label">Spalte 2</span>
<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="ascending"></span>
</button>
<span class="sg-large-table__resize-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Breite von Spalte 2 anpassen" data-large-table-resize-handle="1"></span>
</div>
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="2">
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="2" data-default-width="20%">
<button class="sg-large-table__sort-button" type="button" aria-label="Spalte 3 sortieren">
<span class="sg-large-table__sort-label">Spalte 3</span>
<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="ascending"></span>
</button>
<span class="sg-large-table__resize-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Breite von Spalte 3 anpassen" data-large-table-resize-handle="2"></span>
</div>
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="3">
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="3" data-default-width="20%">
<button class="sg-large-table__sort-button" type="button" aria-label="Spalte 4 sortieren">
<span class="sg-large-table__sort-label">Spalte 4</span>
<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="ascending"></span>
</button>
<span class="sg-large-table__resize-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Breite von Spalte 4 anpassen" data-large-table-resize-handle="3"></span>
</div>
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="4">
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="4" data-default-width="20%">
<button class="sg-large-table__sort-button" type="button" aria-label="Spalte 5 sortieren">
<span class="sg-large-table__sort-label">Spalte 5</span>
<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="ascending"></span>
@@ -210,22 +216,20 @@
<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--load-more" role="row" data-component-part="large-table-row" data-large-table-load-more-row="true">
<div class="sg-large-table__cell sg-large-table__cell--load-more" role="cell">
<div class="sg-navigation-card-layout sg-large-table__load-more-layout">
<div class="sg-navigation-card-block">
<article class="sg-card" data-component="card" data-pattern="navigation-card" aria-label="Navigations-Card">
<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" data-large-table-load-more-trigger="true">Mehr laden</a>
</div>
</div>
</article>
<article class="sg-card sg-large-table__load-more-card" data-component="card" data-pattern="navigation-card" aria-label="Navigations-Card">
<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" data-large-table-load-more-trigger="true">Mehr laden</a>
</div>
</div>
</div>
</article>
</div>
</div>
</div>
</article>
</section>
<script src="../scripts/large-table-column-resize.js"></script>
</body>
</html>
@@ -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>
+19 -1
View File
@@ -7,6 +7,7 @@ Der Styleguide bleibt in diesem Repository die Source of Truth. Der Sync verteil
- den deploy-relevanten CSS-Upstream versioniert als `public/assets/styleguide.upstream.css`
- die vollstaendige Styleguide-Dokumentation gespiegelt nach `docs/styleguide`
- die portal-spezifische Fertig-CSS-Datei als `public/assets/styles.css`
- die konsumierbare Large-Table-Runtime als `public/scripts/large-table-column-resize.js`
Zielrepos:
@@ -31,8 +32,10 @@ Beispiel:
- `public/assets/styleguide.upstream.css` aktualisiert
- `public/assets/styleguide.upstream.meta.json` aktualisiert (Version, Commit, Zeitstempel)
- `docs/styleguide` gespiegelt (mit `--delete`, ohne `.git`, `.codex`, `AGENTS.md`, `scripts/`)
- `docs/styleguide` gespiegelt (mit `--delete`, ohne `.git`, `.codex`, `AGENTS.md` und interne `scripts/`)
- `public/scripts/large-table-column-resize.js` aus der Styleguide-Source-of-Truth kopiert
- `public/assets/styles.css` aktualisiert
- `public/scripts/large-table-column-resize.js` aktualisiert; lokale Portal-Kopien sind nicht zulässig
- Optional: automatischer Commit + Push im Portalrepo
## Standardprozess je Release
@@ -42,3 +45,18 @@ Beispiel:
3. Styleguide commit + push
4. Sync-Skript ausfuehren
5. Beide Portalrepos Smoke-Testen
## Large-Table-Runtime
Die Runtime liegt ausschließlich im Styleguide unter `scripts/large-table-column-resize.js`.
Der Sync kopiert nur diese explizit freigegebene Runtime nach `public/scripts/`; andere
Styleguide-Scripts wie Prüf- und Sync-Shellscripts werden nicht in `docs/styleguide/scripts/`
gespiegelt. Dadurch bleibt der Styleguide die Source of Truth und ein erneuter Sync
überschreibt die Portaldatei kontrolliert.
Für den Smoke-Test in beiden Portalen ist zu prüfen:
- `public/scripts/large-table-column-resize.js` ist vorhanden und unter `/scripts/large-table-column-resize.js` ladbar.
- Die Large-Table-Seite lädt die Runtime ohne lokale Inline- oder Duplikatimplementierung.
- Nach einer Spaltenbreitenänderung wird der Eintrag unter `sg:large-table:column-widths:<key>` gespeichert.
- Ein erneuter Sync ersetzt die Datei aus der Styleguide-Quelle.
+19 -4
View File
@@ -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>
@@ -176,8 +192,7 @@
<tr><td>notifications-card-min-width</td><td>445px</td><td>Mindestbreite der Notification Card im Pattern Notifications; 50px größer als die Object Card und Grundlage für Flex-Basis und Mindestbreite des Patterns.</td></tr>
<tr><td>search-field-width</td><td>15.3rem</td><td>Fixe Breite des Suchfeld-Inputs inklusive Clear-Button-Bereich.</td></tr>
<tr><td>layer-pulldown-panel</td><td>40</td><td>Layer-Stufe für geöffnete Pulldown-Ausklappfelder, damit sie über anderen Inhalten liegen.</td></tr>
<tr><td>multiselect-pulldown-panel-desktop-width</td><td>500px</td><td>Reservierte Foundation-Breite für Multiselektions-Pulldown-Panels; aktuell nicht aktiv genutzt, da das Panel inhaltsbasiert mit Viewport-Kappe skaliert.</td></tr>
<tr><td>multiselect-pulldown-panel-mobile-width</td><td>80vw</td><td>Reservierte mobile Foundation-Breite für Multiselektions-Pulldown-Panels; aktuell nicht aktiv genutzt, da das Panel inhaltsbasiert mit Viewport-Kappe skaliert.</td></tr>
<tr><td>multiselect-pulldown-panel-desktop-width</td><td>500px</td><td>Foundation-Breite des Multiselektions-Pulldown-Panels; wird als feste Pattern-Breite mit Viewport-Kappe wiederverwendet.</td></tr>
<tr><td>options-row-mode-toggle-width</td><td>7rem</td><td>Breite des Modus-Schiebers in der Options Row.</td></tr>
<tr><td>options-row-help-panel-width</td><td>16rem</td><td>Breite des Help-Panels in der Options Row.</td></tr>
<tr><td>card-list-drawer-width</td><td>40%</td><td>Relative Breite des ausziehbaren Card-Listenbereichs.</td></tr>
+1
View File
@@ -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>
+120
View File
@@ -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>
@@ -224,14 +224,16 @@
</div>
<div class="sg-pulldown-panel__row sg-pulldown-panel__row--disabled" data-pulldown-filter-row data-active="false" data-component-part="pulldown-filter-row">
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 1: aus">
<div class="sg-pulldown-panel__row-header">
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 1: aus">
<span class="sg-mode-toggle__switch" aria-hidden="true">
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--left">aus</span>
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--right">an</span>
<span class="sg-mode-toggle__handle"></span>
</span>
</button>
<p class="sg-pulldown-panel__label sg-body">Pulldown 1</p>
</button>
<p class="sg-pulldown-panel__label sg-body">Pulldown 1</p>
</div>
<select class="sg-interaction-element sg-pulldown" aria-label="Pulldown 1 Auswahl" disabled>
<option selected>Option 1</option>
@@ -245,14 +247,16 @@
<h2 id="multiselect-block-4" class="sg-strong sg-form-sections-card__chapter-title">Block 4</h2>
<div class="sg-pulldown-panel__row sg-pulldown-panel__row--disabled" data-pulldown-filter-row data-active="false" data-component-part="pulldown-filter-row">
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 2: aus">
<div class="sg-pulldown-panel__row-header">
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 2: aus">
<span class="sg-mode-toggle__switch" aria-hidden="true">
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--left">aus</span>
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--right">an</span>
<span class="sg-mode-toggle__handle"></span>
</span>
</button>
<p class="sg-pulldown-panel__label sg-body">Pulldown 2</p>
</button>
<p class="sg-pulldown-panel__label sg-body">Pulldown 2</p>
</div>
<select class="sg-interaction-element sg-pulldown" aria-label="Pulldown 2 Auswahl" disabled>
<option selected>Option 1</option>
@@ -262,14 +266,16 @@
</div>
<div class="sg-pulldown-panel__row sg-pulldown-panel__row--disabled" data-pulldown-filter-row data-active="false" data-component-part="pulldown-filter-row">
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 3: aus">
<div class="sg-pulldown-panel__row-header">
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 3: aus">
<span class="sg-mode-toggle__switch" aria-hidden="true">
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--left">aus</span>
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--right">an</span>
<span class="sg-mode-toggle__handle"></span>
</span>
</button>
<p class="sg-pulldown-panel__label sg-body">Pulldown 3</p>
</button>
<p class="sg-pulldown-panel__label sg-body">Pulldown 3</p>
</div>
<select class="sg-interaction-element sg-pulldown" aria-label="Pulldown 3 Auswahl" disabled>
<option selected>Option 1</option>
@@ -279,14 +285,16 @@
</div>
<div class="sg-pulldown-panel__row sg-pulldown-panel__row--disabled" data-pulldown-filter-row data-active="false" data-component-part="pulldown-filter-row">
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 4: aus">
<div class="sg-pulldown-panel__row-header">
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 4: aus">
<span class="sg-mode-toggle__switch" aria-hidden="true">
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--left">aus</span>
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--right">an</span>
<span class="sg-mode-toggle__handle"></span>
</span>
</button>
<p class="sg-pulldown-panel__label sg-body">Pulldown 4</p>
</button>
<p class="sg-pulldown-panel__label sg-body">Pulldown 4</p>
</div>
<select class="sg-interaction-element sg-pulldown" aria-label="Pulldown 4 Auswahl" disabled>
<option selected>Option 1</option>
@@ -390,28 +398,6 @@
);
};
const updateMultiselectLabelAlignment = () => {
document.querySelectorAll('.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel').forEach((panel) => {
const labels = panel.querySelectorAll(
'.sg-slider-row[data-activatable="true"] > .sg-label, .sg-radio-activatable-group > .sg-label, [data-pulldown-filter-row] > .sg-pulldown-panel__label'
);
let maxWidth = 0;
labels.forEach((label) => {
const width = Math.ceil(label.scrollWidth || label.getBoundingClientRect().width);
if (width > maxWidth) {
maxWidth = width;
}
});
if (maxWidth > 0) {
panel.style.setProperty('--sg-multiselect-label-column-width', `${maxWidth}px`);
} else {
panel.style.removeProperty('--sg-multiselect-label-column-width');
}
});
};
document.querySelectorAll('.sg-slider-row').forEach((row) => {
const slider = row.querySelector('.sg-slider');
const valueOutput = row.querySelector('.sg-slider-value');
@@ -500,9 +486,6 @@
});
});
updateMultiselectLabelAlignment();
window.addEventListener('resize', updateMultiselectLabelAlignment);
document.querySelectorAll('.sg-pulldown-demo').forEach((demo) => {
const trigger = demo.querySelector('.sg-pulldown-demo__trigger');
@@ -527,38 +510,12 @@
}
});
demo.dataset.align = 'left';
demo.dataset.open = String(nextState);
trigger.setAttribute('aria-expanded', String(nextState));
if (!nextState) {
return;
}
updateMultiselectLabelAlignment();
const panel = demo.querySelector('.sg-pulldown-panel');
if (!panel) {
return;
}
demo.dataset.align = 'left';
panel.style.left = '0px';
panel.style.right = 'auto';
const initialRect = panel.getBoundingClientRect();
let offsetX = 0;
const maxRight = window.innerWidth;
if (initialRect.right > maxRight) {
offsetX -= initialRect.right - maxRight;
}
if (initialRect.left + offsetX < 0) {
offsetX += 0 - (initialRect.left + offsetX);
}
panel.style.left = `${offsetX}px`;
});
});
@@ -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">
+48 -1
View File
@@ -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; bei ungerader Anzahl steht der letzte Pulldown allein in einer 100-%-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>
@@ -0,0 +1,242 @@
(function initLargeTableColumnResizeModule() {
const STORAGE_PREFIX = 'sg:large-table:column-widths:';
const STORAGE_VERSION = 1;
const DEFAULT_MIN_WIDTH = 128;
const KEYBOARD_STEP = 8;
const KEYBOARD_LARGE_STEP = 40;
let activeResize = null;
const getTables = (root) => root.querySelectorAll('[data-component="large-table"][data-column-resize="true"]');
const getColumnHeaders = (table) => Array.from(
table.querySelectorAll('[data-component-part="large-table-header-row"] [role="columnheader"]'),
);
const getMinWidth = (table) => {
const configured = table.dataset.columnMinWidth;
const value = configured || getComputedStyle(table).getPropertyValue('--large-table-column-min-width').trim();
if (!value) {
return DEFAULT_MIN_WIDTH;
}
const probe = document.createElement('span');
probe.style.cssText = `position:absolute;width:${value};visibility:hidden;`;
table.appendChild(probe);
const width = probe.getBoundingClientRect().width;
probe.remove();
return width > 0 ? width : DEFAULT_MIN_WIDTH;
};
const parseDefaultWidth = (rawValue, tableWidth) => {
if (!rawValue) {
return null;
}
const value = Number.parseFloat(rawValue);
if (!Number.isFinite(value) || value <= 0) {
return null;
}
if (rawValue.trim().endsWith('%')) {
return tableWidth * value / 100;
}
if (rawValue.trim().endsWith('fr')) {
return value;
}
return value;
};
const normalizeWidths = (widths, tableWidth, minWidth) => {
const safeWidths = widths.map((width) => Math.max(minWidth, width));
const total = safeWidths.reduce((sum, width) => sum + width, 0);
if (total <= 0) {
return Array(widths.length).fill(Math.max(minWidth, tableWidth / widths.length));
}
if (widths.some((width) => width !== width)) {
return Array(widths.length).fill(Math.max(minWidth, tableWidth / widths.length));
}
return safeWidths.map((width) => width * tableWidth / total);
};
const getDefaultWidths = (table, headers, minWidth) => {
const tableWidth = Math.max(table.clientWidth, minWidth * headers.length);
const configured = headers.map((header) => parseDefaultWidth(header.dataset.defaultWidth, tableWidth));
if (configured.some((width) => width === null)) {
return Array(headers.length).fill(Math.max(minWidth, tableWidth / headers.length));
}
return normalizeWidths(configured, tableWidth, minWidth);
};
const getStorageKey = (table) => `${STORAGE_PREFIX}${table.dataset.columnResizeKey || ''}`;
const readStoredWidths = (table, count, minWidth) => {
const key = getStorageKey(table);
if (!table.dataset.columnResizeKey) {
return null;
}
try {
if (!window.localStorage) {
return null;
}
const stored = JSON.parse(window.localStorage.getItem(key));
if (stored?.version !== STORAGE_VERSION || !Array.isArray(stored.widths) || stored.widths.length !== count) {
return null;
}
if (stored.widths.some((width) => !Number.isFinite(width) || width < minWidth)) {
return null;
}
return stored.widths;
} catch (error) {
return null;
}
};
const writeStoredWidths = (table, widths) => {
if (!table.dataset.columnResizeKey) {
return;
}
try {
if (!window.localStorage) {
return;
}
window.localStorage.setItem(getStorageKey(table), JSON.stringify({
version: STORAGE_VERSION,
widths: widths.map((width) => Math.round(width)),
}));
} catch (error) {
// Storage can be unavailable in privacy-restricted browser contexts.
}
};
const updateHandleState = (table, widths, minWidth) => {
const handles = table.querySelectorAll('[data-large-table-resize-handle]');
handles.forEach((handle) => {
const index = Number.parseInt(handle.dataset.largeTableResizeHandle, 10);
const current = widths[index];
const maximum = Math.max(minWidth, widths[index] + widths[index + 1] - minWidth);
if (table.id) {
handle.setAttribute('aria-controls', table.id);
}
handle.setAttribute('aria-valuemin', String(Math.round(minWidth)));
handle.setAttribute('aria-valuemax', String(Math.round(maximum)));
handle.setAttribute('aria-valuenow', String(Math.round(current)));
handle.setAttribute('aria-valuetext', `${Math.round(current)} Pixel`);
});
};
const applyWidths = (table, widths, minWidth) => {
const safeWidths = widths.map((width) => Math.max(minWidth, width));
const total = safeWidths.reduce((sum, width) => sum + width, 0);
table.style.setProperty('--large-table-grid-template-columns', safeWidths.map((width) => `${width}px`).join(' '));
table.style.setProperty('--large-table-grid-min-width', `${Math.ceil(total)}px`);
table._largeTableColumnWidths = safeWidths;
updateHandleState(table, safeWidths, minWidth);
};
const resetWidths = (table) => {
const headers = getColumnHeaders(table);
const minWidth = getMinWidth(table);
const widths = getDefaultWidths(table, headers, minWidth);
try {
window.localStorage.removeItem(getStorageKey(table));
} catch (error) {
// Storage can be unavailable in privacy-restricted browser contexts.
}
applyWidths(table, widths, minWidth);
};
const resizeColumn = (table, index, delta, persist) => {
const widths = [...table._largeTableColumnWidths];
const minWidth = getMinWidth(table);
const nextLeft = Math.max(minWidth, Math.min(widths[index] + widths[index + 1] - minWidth, widths[index] + delta));
const appliedDelta = nextLeft - widths[index];
widths[index] = nextLeft;
widths[index + 1] -= appliedDelta;
applyWidths(table, widths, minWidth);
if (persist) {
writeStoredWidths(table, widths);
}
};
const initTable = (table) => {
const headers = getColumnHeaders(table);
if (headers.length < 2) {
return;
}
const minWidth = getMinWidth(table);
const stored = readStoredWidths(table, headers.length, minWidth);
applyWidths(table, stored || getDefaultWidths(table, headers, minWidth), minWidth);
if (table.dataset.largeTableResizeInitialized === 'true') {
return;
}
table.dataset.largeTableResizeInitialized = 'true';
table.addEventListener('pointerdown', (event) => {
const handle = event.target.closest('[data-large-table-resize-handle]');
if (!handle || !table.contains(handle)) {
return;
}
event.preventDefault();
activeResize = {
table,
handle,
index: Number.parseInt(handle.dataset.largeTableResizeHandle, 10),
startX: event.clientX,
startWidths: [...table._largeTableColumnWidths],
};
handle.dataset.largeTableResizeActive = 'true';
table.dataset.columnResizeState = 'resizing';
handle.setPointerCapture?.(event.pointerId);
});
table.addEventListener('pointermove', (event) => {
if (!activeResize || activeResize.table !== table) {
return;
}
applyWidths(table, activeResize.startWidths, getMinWidth(table));
resizeColumn(table, activeResize.index, event.clientX - activeResize.startX, false);
});
table.addEventListener('pointerup', () => {
if (!activeResize || activeResize.table !== table) {
return;
}
writeStoredWidths(table, table._largeTableColumnWidths);
activeResize.handle.dataset.largeTableResizeActive = 'false';
table.dataset.columnResizeState = 'idle';
activeResize = null;
});
table.addEventListener('keydown', (event) => {
const handle = event.target.closest('[data-large-table-resize-handle]');
if (!handle || !table.contains(handle)) {
return;
}
const index = Number.parseInt(handle.dataset.largeTableResizeHandle, 10);
const step = event.shiftKey ? KEYBOARD_LARGE_STEP : KEYBOARD_STEP;
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
event.preventDefault();
resizeColumn(table, index, event.key === 'ArrowRight' ? step : -step, true);
} else if (event.key === 'Home' || event.key === 'End') {
event.preventDefault();
const minWidth = getMinWidth(table);
const current = table._largeTableColumnWidths[index];
const target = event.key === 'Home' ? minWidth : current + table._largeTableColumnWidths[index + 1] - minWidth;
resizeColumn(table, index, target - current, true);
}
});
table.addEventListener('click', (event) => {
const reset = event.target.closest('[data-large-table-reset-widths]');
if (reset && table.contains(reset)) {
resetWidths(table);
}
});
};
window.sgInitLargeTableColumnResize = (options = {}) => {
const root = options.root || document;
getTables(root).forEach(initTable);
};
document.addEventListener('DOMContentLoaded', () => window.sgInitLargeTableColumnResize(), { once: true });
})();
+293 -92
View File
@@ -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();
}
})();
@@ -199,7 +199,6 @@
<tr><td>layout-pulldown-padding-inline</td><td>compact-interaction-padding-horizontal</td><td>Innenabstand links für Pulldown-Trigger und Select-Felder.</td></tr>
<tr><td>layout-pulldown-chevron-offset</td><td>compact-interaction-padding-horizontal</td><td>Rechter Innenabstand des Chevron-Icons im Pulldown.</td></tr>
<tr><td>layout-pulldown-chevron-reserved-space</td><td>spacing-large</td><td>Reservierte Breite zwischen Pulldown-Text und rechtem Chevron-Bereich.</td></tr>
<tr><td>layout-pulldown-panel-form-mobile-width</td><td>90%</td><td>Zielbreite für Pulldown-Panels mit Formularinhalt auf Mobile; wird durch die Viewport-Kappe begrenzt.</td></tr>
<tr><td>layer-pulldown-panel</td><td>dimension-layer-pulldown-panel</td><td>Layer-Stufe geöffneter Pulldown-Ausklappfelder über übrigen Seiteninhalten.</td></tr>
<tr><td>layout-multiselect-pulldown-panel-desktop-width</td><td>dimension-multiselect-pulldown-panel-desktop-width</td><td>Semantische Obergrenze für die Panelbreite geöffneter Pulldowns; das Panel darf über Triggerbreite wachsen, bleibt aber durch diese Breite und die Viewport-Kappe begrenzt.</td></tr>
<tr><td>surface-menu-panel-portal</td><td>color-light-grey</td><td>Fläche des Sandwich-Menü-Panels für große und kleine Variante.</td></tr>
@@ -224,7 +223,7 @@
<tr><td>text-help-icon</td><td>color-font-dark</td><td>Textfarbe im Help-Icon.</td></tr>
<tr><td>text-help-panel</td><td>color-font-dark</td><td>Textfarbe im Help-Panel.</td></tr>
<tr><td>layout-help-panel-width</td><td>calc(interaction-height * 8)</td><td>Standardbreite des Help-Panels.</td></tr>
<tr><td>surface-tooltip</td><td>color-light-grey</td><td>Fläche der generischen Tooltip-Sprechblase.</td></tr>
<tr><td>surface-tooltip</td><td>color-white</td><td>Fläche der generischen Tooltip-Sprechblase.</td></tr>
<tr><td>text-tooltip</td><td>color-font-dark</td><td>Textfarbe der generischen Tooltip-Sprechblase.</td></tr>
<tr><td>layout-tooltip-width</td><td>calc(interaction-height * 8)</td><td>Standardbreite der generischen Tooltip-Sprechblase.</td></tr>
<tr><td>radius-tooltip</td><td>radius-interaction</td><td>Radius der generischen Tooltip-Sprechblase.</td></tr>
+10 -1
View File
@@ -129,7 +129,16 @@
<h3 class="sg-sub-heading sg-section-h3">Multiselektions-Pulldown</h3>
<table class="sg-foundation-table sg-table-label"><thead><tr><th>Semantischer Token</th><th>Verwendeter Foundation-Token</th><th>Beschreibung Verwendungszweck im Pattern</th></tr></thead><tbody>
<tr><td>layout-multiselect-pulldown-label-column-width-fallback</td><td>max-content</td><td>Fallback-Breite der gemeinsamen Labelspalte im Panel, bis die längste Labelbreite per Pattern-Logik ermittelt wurde.</td></tr>
<tr><td>layout-multiselect-panel-width</td><td>layout-multiselect-pulldown-panel-desktop-width</td><td>Feste Breite des Multiselektions-Pulldown-Panels auf allen Viewports innerhalb der vorhandenen Viewport-Kappe.</td></tr>
<tr><td>layout-multiselect-panel-available-width</td><td>Viewportbreite minus 2 × spacing-large</td><td>Viewport-Kappe für das Multiselektions-Pulldown-Panel mit gleichmäßigem Außenabstand.</td></tr>
<tr><td>layout-multiselect-chapter-gap</td><td>spacing-large</td><td>Einheitlicher vertikaler Abstand zwischen allen direkten Elementen eines Multiselect-Blocks.</td></tr>
<tr><td>layout-multiselect-column-gap</td><td>spacing-large</td><td>Horizontaler Abstand zwischen Aktivierungsschalter und Label in den Pattern-Zeilen.</td></tr>
<tr><td>layout-multiselect-control-gap</td><td>spacing-small</td><td>Standardabstand innerhalb der Slider-Zeilen.</td></tr>
<tr><td>layout-multiselect-visual-control-gap</td><td>2 × spacing-small</td><td>Visueller Abstand zwischen Kopfzeile und Interaktionselement bei Radio- und Pulldown-Zeilen.</td></tr>
<tr><td>layout-multiselect-control-row-height</td><td>interaction-height</td><td>Höhe der Kopfzeile in den filterbaren Pulldown-Zeilen.</td></tr>
<tr><td>layout-multiselect-value-min-width</td><td>1.25 × interaction-height</td><td>Mindestbreite der Slider-Wertanzeige.</td></tr>
<tr><td>typography-multiselect-label-line-height</td><td>line-height-base</td><td>Zeilenhöhe der Labels in den filterbaren Pulldown-Zeilen.</td></tr>
<tr><td>opacity-multiselect-disabled</td><td>disabled-opacity</td><td>Deckkraft deaktivierter Multiselect-Inhalte.</td></tr>
</tbody></table>
<h3 class="sg-sub-heading sg-section-h3">Overlay Card</h3>
+1
View File
@@ -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";
+3 -5
View File
@@ -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;
@@ -62,7 +64,7 @@
--surface-menu-panel-portal: var(--color-light-grey);
--surface-help-icon: var(--color-medium-grey);
--surface-help-panel: var(--color-light-grey);
--surface-tooltip: var(--color-light-grey);
--surface-tooltip: var(--color-white);
--surface-pulldown-panel: var(--color-light-grey);
--surface-activatable-remove: var(--surface-control-default);
--surface-slider-track: var(--color-medium-grey);
@@ -77,10 +79,8 @@
--layout-pulldown-chevron-offset: var(--compact-interaction-padding-horizontal);
--layout-tab-navigation-large-padding-inline: var(--interaction-padding-horizontal);
--layout-pulldown-chevron-reserved-space: var(--spacing-large);
--layout-pulldown-panel-form-mobile-width: 90%;
--layer-pulldown-panel: var(--dimension-layer-pulldown-panel);
--layout-multiselect-pulldown-panel-desktop-width: var(--dimension-multiselect-pulldown-panel-desktop-width);
--layout-multiselect-pulldown-label-column-width-fallback: max-content;
--layout-input-label-width: var(--dimension-input-label-width);
--layout-input-field-desktop-width: var(--dimension-input-field-desktop-width);
--layout-input-field-max-width: var(--dimension-input-field-max-width);
@@ -144,7 +144,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 +309,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;
+1 -1
View File
@@ -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 {
@@ -1297,33 +1295,74 @@
}
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel::before {
top: calc(var(--spacing-small) * -1);
top: calc(var(--spacing-small) * -2);
left: 50%;
border-width: 0 var(--spacing-small) var(--spacing-small);
border-width: 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
border-color: transparent transparent var(--surface-tooltip);
transform: translateX(-50%);
}
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel::before {
bottom: calc(var(--spacing-small) * -1);
bottom: calc(var(--spacing-small) * -2);
left: 50%;
border-width: var(--spacing-small) var(--spacing-small) 0;
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
border-color: var(--surface-tooltip) transparent transparent;
transform: translateX(-50%);
}
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel::before {
top: 50%;
right: calc(var(--spacing-small) * -1);
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
right: calc(var(--spacing-small) * -2);
border-width: calc(var(--spacing-small) * 2) 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
border-color: transparent transparent transparent var(--surface-tooltip);
transform: translateY(-50%);
}
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel::before {
top: 50%;
left: calc(var(--spacing-small) * -1);
border-width: var(--spacing-small) var(--spacing-small) var(--spacing-small) 0;
left: calc(var(--spacing-small) * -2);
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
border-color: transparent var(--surface-tooltip) transparent transparent;
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) * -2);
left: var(--tooltip-arrow-offset);
border-width: 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
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) * -2);
left: var(--tooltip-arrow-offset);
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 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) * -2);
border-width: calc(var(--spacing-small) * 2) 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
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) * -2);
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
border-color: transparent var(--surface-tooltip) transparent transparent;
transform: translateY(-50%);
}
@@ -91,19 +91,44 @@
.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:not(.sg-options-row--left-only) > .sg-options-row__left:has(> .sg-pulldown-demo:nth-last-child(2):nth-child(odd)) > .sg-pulldown-demo:nth-last-child(2),
.sg-options-row:not(.sg-options-row--left-only) > .sg-options-row__left:has(> .sg-pulldown-demo:last-child:nth-child(odd)) > .sg-pulldown-demo:last-child {
flex-basis: 100%;
}
.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,
@@ -111,15 +136,6 @@
width: 100%;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
width: min(
var(--layout-pulldown-panel-form-mobile-width),
calc(100vw - (2 * var(--spacing-large)))
);
min-width: 0;
max-width: calc(100vw - (2 * var(--spacing-large)));
}
.sg-options-row .sg-pulldown-panel .sg-form-sections-card-wrapper,
.sg-options-row .sg-pulldown-panel .sg-form-sections-card {
min-width: 0;
@@ -75,39 +75,63 @@
padding-left: var(--layout-form-sections-interaction-indent);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > :not(.sg-form-sections-card__chapter-title) + :not(.sg-form-sections-card__chapter-title) {
margin-top: var(--spacing-large);
.sg-options-row[data-pattern="multiselektions-pulldown"] {
--layout-multiselect-panel-width: var(--layout-multiselect-pulldown-panel-desktop-width);
--layout-multiselect-panel-available-width: calc(100vw - (2 * var(--spacing-large)));
--layout-multiselect-chapter-gap: var(--spacing-large);
--layout-multiselect-column-gap: var(--spacing-large);
--layout-multiselect-control-gap: var(--spacing-small);
--layout-multiselect-visual-control-gap: calc(var(--spacing-small) * 2);
--layout-multiselect-control-row-height: var(--interaction-height);
--layout-multiselect-value-min-width: calc(var(--interaction-height) * 1.25);
--typography-multiselect-label-line-height: var(--line-height-base);
--opacity-multiselect-disabled: var(--disabled-opacity);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-form-sections-card__chapter-title + * {
margin-top: var(--spacing-large);
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > * + * {
margin-top: var(--layout-multiselect-chapter-gap);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-slider-row + .sg-slider-row,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-checkbox-field-option + .sg-checkbox-field-option,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-radio-activatable-group + .sg-radio-activatable-group {
margin-top: var(--spacing-small);
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-options-row__left > .sg-pulldown-demo {
flex: 1 1 100%;
min-width: 0;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-pulldown-panel__row + .sg-pulldown-panel__row {
margin-top: var(--spacing-large);
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-options-row__left > .sg-pulldown-demo .sg-pulldown {
width: 100%;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
--sg-multiselect-label-column-width: var(--layout-multiselect-pulldown-label-column-width-fallback);
width: min(
var(--layout-multiselect-panel-width),
var(--layout-multiselect-panel-available-width)
);
min-width: min(
var(--layout-multiselect-panel-width),
var(--layout-multiselect-panel-available-width)
);
max-width: var(--layout-multiselect-panel-available-width);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel .sg-form-sections-card-wrapper,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel .sg-form-sections-card {
min-width: 0;
width: 100%;
max-width: 100%;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] > .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
white-space: nowrap;
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] .sg-pulldown-panel__label {
white-space: normal;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
display: grid;
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr) auto;
grid-template-columns: max-content minmax(0, 1fr) auto;
align-items: center;
column-gap: var(--spacing-large);
column-gap: var(--layout-multiselect-column-gap);
row-gap: var(--layout-multiselect-control-gap);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable {
@@ -115,37 +139,43 @@
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-slider {
opacity: var(--disabled-opacity);
opacity: var(--opacity-multiselect-disabled);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-slider-value,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-radio-activatable-group__choices {
opacity: var(--disabled-opacity);
opacity: var(--opacity-multiselect-disabled);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-mode-toggle {
grid-column: 1;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-label {
grid-column: 2;
grid-row: 1;
min-width: 0;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider {
grid-column: 3;
grid-column: 1 / 3;
grid-row: 2;
min-width: 0;
max-width: 100%;
width: 100%;
max-width: none;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider-value {
grid-column: 4;
grid-column: 3;
grid-row: 2;
margin: 0;
min-width: 0;
min-width: var(--layout-multiselect-value-min-width);
line-height: 1;
align-self: center;
white-space: nowrap;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
@@ -154,22 +184,26 @@
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group {
display: grid;
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr);
grid-template-columns: max-content minmax(0, 1fr);
align-items: center;
column-gap: var(--spacing-large);
column-gap: var(--layout-multiselect-column-gap);
row-gap: var(--layout-multiselect-visual-control-gap);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label {
grid-column: 2;
grid-row: 1;
margin: 0;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-mode-toggle {
grid-column: 1;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-radio-activatable-group__choices {
grid-column: 3;
grid-column: 1 / -1;
grid-row: 2;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-radio-activatable-group__choices {
@@ -178,119 +212,45 @@
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable > :not(.sg-mode-toggle),
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > :not(.sg-mode-toggle),
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > :not(.sg-mode-toggle) {
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__row-header > :not(.sg-mode-toggle),
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown {
pointer-events: none;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] {
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr);
column-gap: var(--spacing-large);
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: var(--layout-multiselect-control-row-height) auto;
row-gap: var(--layout-multiselect-visual-control-gap);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-mode-toggle {
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__row-header {
display: flex;
grid-column: 1;
grid-row: 1;
align-items: center;
gap: var(--layout-multiselect-column-gap);
min-width: 0;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
grid-column: 2;
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__row-header > .sg-pulldown-panel__label {
margin: 0;
min-width: 0;
line-height: var(--typography-multiselect-label-line-height);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown {
grid-column: 3;
grid-column: 1;
grid-row: 2;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled {
opacity: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__label,
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__row-header > .sg-pulldown-panel__label,
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown {
opacity: var(--disabled-opacity);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-demo .sg-pulldown-panel {
min-width: 100%;
width: max-content;
max-width: calc(100vw - (2 * var(--spacing-large)));
}
@media (max-width: 48rem) {
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
min-width: 100%;
width: max-content;
max-width: calc(100vw - (2 * var(--spacing-large)));
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
grid-template-columns: max-content minmax(0, 1fr) auto;
row-gap: var(--spacing-small);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] > .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
white-space: normal;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-mode-toggle {
grid-column: 1;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-label {
grid-column: 2;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider {
grid-column: 1 / 3;
grid-row: 2;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider-value {
grid-column: 3;
grid-row: 2;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group {
grid-template-columns: max-content minmax(0, 1fr);
row-gap: var(--spacing-small);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label {
grid-column: 2;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-mode-toggle {
grid-column: 1;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-radio-activatable-group__choices {
grid-column: 1 / -1;
grid-row: 2;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] {
grid-template-columns: max-content minmax(0, 1fr);
row-gap: var(--spacing-small);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-mode-toggle {
grid-column: 1;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
grid-column: 2;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown {
grid-column: 1 / -1;
grid-row: 2;
}
opacity: var(--opacity-multiselect-disabled);
}
.sg-form-sections-card__actions {
+14 -4
View File
@@ -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);
@@ -71,16 +71,32 @@
width: 100%;
}
.sg-large-table[data-column-resize="true"] {
overflow: hidden;
}
.sg-large-table__viewport {
align-self: stretch;
flex: 1 1 auto;
min-width: 0;
width: auto;
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
box-sizing: border-box;
}
.sg-large-table__title-segment {
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: var(--spacing-large);
box-sizing: border-box;
}
.sg-large-table__row {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
grid-template-columns: var(--large-table-grid-template-columns, repeat(5, minmax(0, 1fr)));
align-items: stretch;
gap: 0;
width: 100%;
@@ -88,6 +104,11 @@
background: var(--color-white);
}
.sg-large-table[data-column-resize="true"] .sg-large-table__row,
.sg-large-table[data-column-resize="true"] .sg-large-table__title-segment {
width: max(100%, var(--large-table-grid-min-width, 0px));
}
.sg-large-table__row--hidden {
display: none;
}
@@ -119,16 +140,59 @@
white-space: normal;
}
.sg-large-table__load-more-layout {
.sg-large-table__row--load-more,
.sg-large-table__cell--load-more,
.sg-large-table__load-more-card {
min-width: 0;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.sg-large-table[data-column-resize="true"] .sg-large-table__row--load-more {
grid-template-columns: minmax(0, 1fr);
}
.sg-large-table__cell--header {
position: relative;
padding: 0;
}
.sg-large-table__resize-handle {
position: absolute;
z-index: 1;
top: 0;
right: -0.25rem;
bottom: 0;
width: 0.5rem;
cursor: col-resize;
touch-action: none;
}
.sg-large-table__resize-handle::after {
content: "";
position: absolute;
top: 25%;
bottom: 25%;
left: 50%;
width: 0.1rem;
transform: translateX(-50%);
background: var(--color-darkblue);
box-shadow: -0.15rem 0 0 var(--color-darkblue), 0.15rem 0 0 var(--color-darkblue);
}
.sg-large-table__resize-handle:hover::after,
.sg-large-table__resize-handle:focus-visible::after,
.sg-large-table[data-column-resize-state="resizing"] .sg-large-table__resize-handle[data-large-table-resize-active="true"]::after {
background: var(--color-darkblue);
box-shadow: -0.2rem 0 0 var(--color-darkblue), 0.2rem 0 0 var(--color-darkblue);
}
.sg-large-table__resize-handle:focus-visible {
outline: 2px solid var(--color-darkblue);
outline-offset: -1px;
}
.sg-large-table__sort-button {
display: flex;
align-items: center;
@@ -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%;
}
+10
View File
@@ -394,6 +394,8 @@ function get_sales_order_detail(PDO $pdo, int $orderId): ?array
so.amount_discount,
so.total_amount,
so.currency,
md.name AS market_name,
mday.date AS market_occurrence_day_date,
COALESCE(ad.first_name, '') AS first_name,
COALESCE(ad.last_name, '') AS last_name,
COALESCE(ad.street, '') AS street,
@@ -413,6 +415,10 @@ function get_sales_order_detail(PDO $pdo, int $orderId): ?array
) ad ON TRUE
LEFT JOIN payment_method pm ON pm.id = so.payment_method_id
LEFT JOIN shipping_method sm ON sm.id = so.shipping_method_id
LEFT JOIN market_occurrence mo ON mo.id = so.market_occurrence_id
LEFT JOIN market_definition md ON md.id = mo.market_definition_id
LEFT JOIN market_occurrence_day mday
ON mday.id = so.market_occurrence_day_id AND mday.occurrence_id = mo.id
WHERE so.id = :order_id"
);
$orderStmt->execute([':order_id' => $orderId]);
@@ -499,6 +505,10 @@ function get_sales_order_detail(PDO $pdo, int $orderId): ?array
'payment_status' => (string) $order['payment_status'],
'payment_method' => (string) $order['payment_method'],
'shipping_method' => (string) $order['shipping_method'],
'market' => [
'name' => $order['market_name'] !== null ? (string) $order['market_name'] : null,
'occurrence_day_date' => $order['market_occurrence_day_date'] !== null ? (string) $order['market_occurrence_day_date'] : null,
],
'customer' => [
'first_name' => (string) $order['first_name'],
'last_name' => (string) $order['last_name'],
+91
View File
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/service.php';
function erp_lager_mcp_token(array $env): string
{
$token = env_value('MARKET_MCP_TOKEN', $env);
if ($token === '') {
throw new RuntimeException('MARKET_MCP_TOKEN ist nicht konfiguriert.');
}
return $token;
}
function erp_lager_mcp_authorize(array $env): string
{
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!preg_match('/^Bearer\s+(.+)$/i', $header, $matches)) {
throw new RuntimeException('MCP Authorization Bearer Token fehlt.');
}
$token = trim($matches[1]);
if (!hash_equals(erp_lager_mcp_token($env), $token)) {
throw new RuntimeException('MCP Authorization Bearer Token ist ungültig.');
}
return 'hermes';
}
function erp_lager_mcp_tools(): array
{
return [
[
'name' => 'erp_lager.list_products',
'description' => 'Aktive ERP-Lagerprodukte zur Ermittlung der Produkt-IDs lesen',
'inputSchema' => ['type' => 'object', 'properties' => (object) []],
],
[
'name' => 'erp_lager.get_product',
'description' => 'Produkt, aktuelle Lagermenge, Chargen und Ausverkaufsprognose lesen',
'inputSchema' => [
'type' => 'object',
'required' => ['product_id'],
'properties' => [
'product_id' => ['type' => 'integer'],
],
],
],
];
}
function erp_lager_mcp_list_products(PDO $pdo): array
{
return array_map(
static fn (array $product): array => [
'id' => $product['id'],
'name' => $product['name'],
'sku' => $product['sku'],
'uom' => $product['uom'],
],
get_otc_order_form_products($pdo)
);
}
function erp_lager_mcp_get_product(PDO $pdo, int $productId): array
{
if ($productId < 1) {
throw new InvalidArgumentException('product_id muss eine positive Ganzzahl sein.');
}
foreach (get_lager_product_overview($pdo) as $product) {
if ((int) $product['id'] === $productId) {
return $product;
}
}
throw new InvalidArgumentException('Aktives Lagerprodukt nicht gefunden.');
}
function erp_lager_mcp_call(PDO $pdo, string $tool, array $arguments): array
{
return match ($tool) {
'erp_lager.list_products' => erp_lager_mcp_list_products($pdo),
'erp_lager.get_product' => erp_lager_mcp_get_product(
$pdo,
(int) ($arguments['product_id'] ?? 0)
),
default => throw new InvalidArgumentException('Unbekanntes Lager-MCP-Tool: ' . $tool),
};
}
+32 -17
View File
@@ -30,11 +30,13 @@ function market_management_mcp_authorize(array $env): string
function market_management_mcp_tools(): array
{
$idempotencyProperty = ['idempotency_key' => ['type' => 'string', 'minLength' => 1]];
$occurrenceProperties = [
'status' => ['type' => 'string'], 'place' => ['type' => ['string', 'null']], 'address' => ['type' => ['string', 'null']],
'municipality_reference_id' => ['type' => ['integer', 'null']], 'notes' => ['type' => ['string', 'null']],
'registration_deadline' => ['type' => ['string', 'null'], 'format' => 'date'],
'request_or_registration_date' => ['type' => ['string', 'null'], 'format' => 'date'],
'registration_note' => ['type' => ['string', 'null']],
'confirmation_date' => ['type' => ['string', 'null'], 'format' => 'date'],
'payment_deadline' => ['type' => ['string', 'null'], 'format' => 'date'],
'payment_status' => ['type' => ['string', 'null'], 'enum' => ['paid', 'unpaid']],
@@ -46,7 +48,7 @@ function market_management_mcp_tools(): array
'power_available' => ['type' => ['boolean', 'null']], 'water_available' => ['type' => ['boolean', 'null']],
'lighting_available' => ['type' => ['boolean', 'null']], 'other_infrastructure' => ['type' => ['string', 'null']],
'own_stand_requirements' => ['type' => ['string', 'null']], 'travel_address' => ['type' => ['string', 'null']],
'travel_distance_km' => ['type' => ['number', 'null']], 'travel_duration_minutes' => ['type' => ['integer', 'null']],
'travel_distance_km' => ['type' => ['number', 'null']],
'parking_options' => ['type' => ['string', 'null']], 'delivery_access' => ['type' => ['string', 'null']],
'direct_unload_possible' => ['type' => ['boolean', 'null']], 'storage_options' => ['type' => ['string', 'null']],
'travel_notes' => ['type' => ['string', 'null']], 'cancellation_terms' => ['type' => ['string', 'null']],
@@ -74,24 +76,26 @@ function market_management_mcp_tools(): array
['name' => 'market_management.search_markets', 'description' => 'Märkte suchen', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]],
['name' => 'market_management.search_occurrences', 'description' => 'Marktdurchführungen suchen und auflisten', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]],
['name' => 'market_management.get_market', 'description' => 'Markt mit Marktdurchführungen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]],
['name' => 'market_management.create_market', 'description' => 'Markt mit verpflichtendem Veranstalter, Kategorie und optionaler Website-URL anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'category_id', 'organizer_id'], 'properties' => ['name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]],
['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.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.create_municipality', 'description' => 'Lokale Gemeinde im Markt-Gemeinderegister anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name'], 'properties' => ['name' => ['type' => 'string']]]],
['name' => 'market_management.create_market', 'description' => 'Markt mit verpflichtendem Veranstalter, Kategorie und optionaler Website-URL anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'category_id', 'organizer_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null']]]]],
['name' => 'market_management.update_market', 'description' => 'Markt inklusive Veranstalter und Website-URL ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['market_id' => ['type' => 'integer'], 'name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null']]]]],
['name' => 'market_management.archive_market', 'description' => 'Markt archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['market_id' => ['type' => 'integer']]]],
['name' => 'market_management.reactivate_market', 'description' => 'Markt reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['market_id' => ['type' => 'integer']]]],
['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', 'idempotency_key'], 'properties' => $idempotencyProperty + ['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' => (object) []]],
['name' => 'market_management.create_municipality', 'description' => 'Lokale Gemeinde im Markt-Gemeinderegister anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'idempotency_key'], 'properties' => $idempotencyProperty + ['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]]],
['name' => 'market_management.create_occurrence', 'description' => 'Marktdurchführung mit mindestens einem Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id', 'days', 'idempotency_key'], 'properties' => $idempotencyProperty + ['market_definition_id' => ['type' => 'integer'], 'status' => ['type' => 'string']] + $occurrenceProperties + ['days' => ['type' => 'array', 'minItems' => 1, 'items' => ['type' => 'object', 'required' => ['date'], 'properties' => $dayProperties]]]]],
['name' => 'market_management.get_occurrence', 'description' => 'Marktdurchführung mit Markt, Markttagen und berechneten Verkaufsdaten lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]],
['name' => 'market_management.update_occurrence', 'description' => 'Marktdurchführung ändern', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer']] + $occurrenceProperties]],
['name' => 'market_management.archive_occurrence', 'description' => 'Marktdurchführung archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer']]]],
['name' => 'market_management.reactivate_occurrence', 'description' => 'Marktdurchführung reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer']]]],
['name' => 'market_management.create_day', 'description' => 'Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'date'], 'properties' => ['occurrence_id' => ['type' => 'integer']] + $dayProperties]],
['name' => 'market_management.update_day', 'description' => 'Markttag ändern', 'inputSchema' => ['type' => 'object', 'required' => ['day_id'], 'properties' => ['day_id' => ['type' => 'integer']] + $dayProperties]],
['name' => 'market_management.delete_day', 'description' => 'Markttag entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['day_id'], 'properties' => ['day_id' => ['type' => 'integer']]]],
['name' => 'market_management.get_occurrence_checklist', 'description' => 'Checkliste einer Marktdurchführung lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]],
['name' => 'market_management.update_occurrence_checklist', 'description' => 'Checkliste einer Marktdurchführung vollständig ersetzen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'items', 'idempotency_key'], 'properties' => $idempotencyProperty + ['occurrence_id' => ['type' => 'integer'], 'comment' => ['type' => ['string', 'null']], 'items' => ['type' => 'array', 'items' => ['type' => 'object', 'required' => ['chapter_key', 'label', 'is_completed'], 'properties' => ['chapter_key' => ['type' => 'string', 'enum' => ['products_sales', 'decoration', 'stand_equipment', 'technology']], 'label' => ['type' => 'string'], 'is_completed' => ['type' => 'boolean']]]]]]],
['name' => 'market_management.update_occurrence', 'description' => 'Marktdurchführung ändern', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['occurrence_id' => ['type' => 'integer']] + $occurrenceProperties]],
['name' => 'market_management.archive_occurrence', 'description' => 'Marktdurchführung archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['occurrence_id' => ['type' => 'integer']]]],
['name' => 'market_management.reactivate_occurrence', 'description' => 'Marktdurchführung reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['occurrence_id' => ['type' => 'integer']]]],
['name' => 'market_management.create_day', 'description' => 'Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'date', 'idempotency_key'], 'properties' => $idempotencyProperty + ['occurrence_id' => ['type' => 'integer']] + $dayProperties]],
['name' => 'market_management.update_day', 'description' => 'Markttag ändern', 'inputSchema' => ['type' => 'object', 'required' => ['day_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['day_id' => ['type' => 'integer']] + $dayProperties]],
['name' => 'market_management.delete_day', 'description' => 'Markttag entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['day_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['day_id' => ['type' => 'integer']]]],
];
}
@@ -103,6 +107,7 @@ function market_management_mcp_is_write_tool(string $tool): bool
'market_management.create_municipality',
'market_management.archive_market', 'market_management.reactivate_market',
'market_management.create_occurrence', 'market_management.update_occurrence',
'market_management.update_occurrence_checklist',
'market_management.archive_occurrence', 'market_management.reactivate_occurrence',
'market_management.create_day', 'market_management.update_day', 'market_management.delete_day',
], true);
@@ -302,6 +307,16 @@ function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): a
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
(bool) ($arguments['include_archived'] ?? false)
),
'market_management.get_occurrence_checklist' => market_management_get_occurrence_checklist(
$pdo,
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
(bool) ($arguments['include_archived'] ?? false)
),
'market_management.update_occurrence_checklist' => market_management_update_occurrence_checklist(
$pdo,
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
$arguments
),
'market_management.update_occurrence' => market_management_mcp_update_occurrence(
$pdo,
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
const MARKET_OCCURRENCE_CHECKLIST_CHAPTERS = [
[
'key' => 'products_sales',
'label' => 'Produkte & Verkauf',
'items' => ['Fläschchen', 'Säckchen', 'Dunkle Kleber', 'Naurua-Kleber', 'Anwendungsbeilage', 'Flyer Naurua', 'Flyer Vitalpilze', 'Vitalpilzbuch', 'SumUp-Terminal', 'Bargeld'],
],
[
'key' => 'decoration',
'label' => 'Dekoration',
'items' => ['Dekoration', 'Pilze', 'Blumenvase', 'Blume'],
],
[
'key' => 'stand_equipment',
'label' => 'Stand & Ausstattung',
'items' => ['Tisch', 'Tischtücher', 'Beleuchtung', 'Holzklötze', 'Wagen', 'Sonnenschirm', 'Parkkarte/-Marktinformationen'],
],
[
'key' => 'technology',
'label' => 'Technik',
'items' => ['Batterie-/Powerbank fürs Handy'],
],
];
function market_management_checklist_default_items(): array
{
$items = [];
foreach (MARKET_OCCURRENCE_CHECKLIST_CHAPTERS as $chapter) {
foreach ($chapter['items'] as $label) {
$items[] = [
'chapter_key' => $chapter['key'],
'label' => $label,
];
}
}
return $items;
}
function market_management_checklist_assert_occurrence(PDO $pdo, int $occurrenceId, bool $includeArchived = false): void
{
$predicate = $includeArchived ? 'TRUE' : 'archived_at IS NULL';
$stmt = $pdo->prepare('SELECT id FROM market_occurrence WHERE id = :id AND ' . $predicate);
$stmt->execute([':id' => $occurrenceId]);
if ($stmt->fetchColumn() === false) {
throw new InvalidArgumentException('occurrence_id verweist nicht auf eine Marktdurchführung.');
}
}
function market_management_checklist_initialize(PDO $pdo, int $occurrenceId): void
{
market_management_checklist_assert_occurrence($pdo, $occurrenceId, true);
$parent = $pdo->prepare(
'INSERT INTO market_occurrence_checklist (occurrence_id)
VALUES (:occurrence_id)
ON CONFLICT (occurrence_id) DO NOTHING'
);
$parent->execute([':occurrence_id' => $occurrenceId]);
$existing = $pdo->prepare('SELECT COUNT(*) FROM market_occurrence_checklist_item WHERE occurrence_id = :occurrence_id');
$existing->execute([':occurrence_id' => $occurrenceId]);
if ((int) $existing->fetchColumn() > 0) {
return;
}
$insert = $pdo->prepare(
'INSERT INTO market_occurrence_checklist_item
(occurrence_id, chapter_key, position, label, is_completed)
VALUES (:occurrence_id, :chapter_key, :position, :label, :is_completed)'
);
$chapterPositions = [];
foreach (market_management_checklist_default_items() as $item) {
$insert->bindValue(':occurrence_id', $occurrenceId, PDO::PARAM_INT);
$insert->bindValue(':chapter_key', $item['chapter_key'], PDO::PARAM_STR);
$chapterPositions[$item['chapter_key']] = ($chapterPositions[$item['chapter_key']] ?? 0) + 1;
$insert->bindValue(':position', $chapterPositions[$item['chapter_key']], PDO::PARAM_INT);
$insert->bindValue(':label', $item['label'], PDO::PARAM_STR);
$insert->bindValue(':is_completed', false, PDO::PARAM_BOOL);
$insert->execute();
}
}
function market_management_get_occurrence_checklist(PDO $pdo, int $occurrenceId, bool $includeArchived = false): array
{
market_management_checklist_assert_occurrence($pdo, $occurrenceId, $includeArchived);
$parent = $pdo->prepare(
'SELECT id, occurrence_id, comment, created_at, updated_at
FROM market_occurrence_checklist
WHERE occurrence_id = :occurrence_id'
);
$parent->execute([':occurrence_id' => $occurrenceId]);
$checklist = $parent->fetch();
if (!is_array($checklist)) {
throw new RuntimeException('Checkliste der Marktdurchführung fehlt.');
}
$items = $pdo->prepare(
'SELECT id, occurrence_id, chapter_key, position, label, is_completed, created_at, updated_at
FROM market_occurrence_checklist_item
WHERE occurrence_id = :occurrence_id
ORDER BY id ASC'
);
$items->execute([':occurrence_id' => $occurrenceId]);
$checklist['items'] = $items->fetchAll();
$checklist['chapters'] = array_map(
static fn (array $chapter): array => ['key' => $chapter['key'], 'label' => $chapter['label']],
MARKET_OCCURRENCE_CHECKLIST_CHAPTERS
);
return $checklist;
}
function market_management_update_occurrence_checklist(PDO $pdo, int $occurrenceId, array $input): array
{
market_management_checklist_assert_occurrence($pdo, $occurrenceId, true);
$items = $input['items'] ?? null;
if (!is_array($items)) {
throw new InvalidArgumentException('items muss eine Liste sein.');
}
$comment = $input['comment'] ?? null;
if ($comment !== null && !is_string($comment)) {
throw new InvalidArgumentException('comment muss ein String oder null sein.');
}
$startedTransaction = !$pdo->inTransaction();
if ($startedTransaction) {
$pdo->beginTransaction();
}
try {
$parent = $pdo->prepare(
'INSERT INTO market_occurrence_checklist (occurrence_id, comment)
VALUES (:occurrence_id, :comment)
ON CONFLICT (occurrence_id) DO UPDATE
SET comment = EXCLUDED.comment, updated_at = NOW()'
);
$parent->execute([':occurrence_id' => $occurrenceId, ':comment' => $comment === null ? null : trim($comment)]);
$delete = $pdo->prepare('DELETE FROM market_occurrence_checklist_item WHERE occurrence_id = :occurrence_id');
$delete->execute([':occurrence_id' => $occurrenceId]);
$insert = $pdo->prepare(
'INSERT INTO market_occurrence_checklist_item
(occurrence_id, chapter_key, position, label, is_completed)
VALUES (:occurrence_id, :chapter_key, :position, :label, :is_completed)'
);
$chapterKeys = array_fill_keys(array_column(MARKET_OCCURRENCE_CHECKLIST_CHAPTERS, 'key'), true);
$chapterPositions = [];
foreach (array_values($items) as $position => $item) {
if (!is_array($item)) {
throw new InvalidArgumentException('Jeder Checklistenpunkt muss ein Objekt sein.');
}
$chapterKey = $item['chapter_key'] ?? null;
if (!is_string($chapterKey) || !isset($chapterKeys[$chapterKey])) {
throw new InvalidArgumentException('chapter_key verweist nicht auf ein festes Checklisten-Kapitel.');
}
$label = $item['label'] ?? '';
if (!is_string($label)) {
throw new InvalidArgumentException('Der Text eines Checklistenpunkts muss ein String sein.');
}
$completed = $item['is_completed'] ?? false;
if (!is_bool($completed)) {
throw new InvalidArgumentException('is_completed muss boolean sein.');
}
$insert->bindValue(':occurrence_id', $occurrenceId, PDO::PARAM_INT);
$chapterPositions[$chapterKey] = ($chapterPositions[$chapterKey] ?? 0) + 1;
$insert->bindValue(':chapter_key', $chapterKey, PDO::PARAM_STR);
$insert->bindValue(':position', $chapterPositions[$chapterKey], PDO::PARAM_INT);
$insert->bindValue(':label', trim($label), PDO::PARAM_STR);
$insert->bindValue(':is_completed', $completed, PDO::PARAM_BOOL);
$insert->execute();
}
if ($startedTransaction) {
$pdo->commit();
}
} catch (Throwable $error) {
if ($startedTransaction && $pdo->inTransaction()) {
$pdo->rollBack();
}
throw $error;
}
return market_management_get_occurrence_checklist($pdo, $occurrenceId, true);
}
+185 -8
View File
@@ -2,6 +2,7 @@
declare(strict_types=1);
require_once __DIR__ . '/../shared/db.php';
require_once __DIR__ . '/occurrence_checklist.php';
const MARKET_MANAGEMENT_STATUSES = [
'open',
@@ -18,6 +19,126 @@ const MARKET_OCCURRENCE_DAY_INHERITED_FIELDS = [
'delivery_start_time', 'delivery_end_time',
];
const MARKET_MANAGEMENT_MULTILINE_FIELDS = [
'address', 'notes', 'registration_note', '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_NOQUOTES | 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_NOQUOTES | 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_NOQUOTES | 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(
@@ -232,6 +353,10 @@ function market_management_optional_url(array $input, string $field): ?string
return null;
}
if (!preg_match('/^https?:\/\//i', $value)) {
$value = 'https://' . $value;
}
if (filter_var($value, FILTER_VALIDATE_URL) === false || !preg_match('/^https?:\/\//i', $value)) {
throw new InvalidArgumentException($field . ' muss eine gültige HTTP(S)-URL sein.');
}
@@ -378,6 +503,11 @@ function market_management_search_markets(PDO $pdo, array $input): array
$sortColumn = match ($sort) {
'earliest_date' => 'MIN(d.date)',
'updated_at' => 'm.updated_at',
'category_name' => 'c.name',
'organizer_name' => 'p.name',
'website_url' => 'm.website_url',
'occurrence_count' => 'COUNT(DISTINCT o.id)',
'status' => 'CASE WHEN m.archived_at IS NULL THEN 0 ELSE 1 END',
default => 'm.name',
};
@@ -482,6 +612,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 +852,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;
}
@@ -759,12 +893,12 @@ function market_management_create_occurrence(PDO $pdo, array $input): array
}
$definitions = [
'place' => 'string', 'address' => 'string', 'municipality_reference_id' => 'id', 'notes' => 'string',
'registration_deadline' => 'string', 'request_or_registration_date' => 'string', 'confirmation_date' => 'string',
'registration_deadline' => 'string', 'request_or_registration_date' => 'string', 'registration_note' => 'string', 'confirmation_date' => 'string',
'payment_deadline' => 'string', 'stand_size' => 'string', 'table_available' => 'bool',
'table_rentable' => 'bool', 'own_table_required' => 'bool', 'shelter_available' => 'bool',
'power_available' => 'bool', 'water_available' => 'bool', 'lighting_available' => 'bool',
'other_infrastructure' => 'string', 'own_stand_requirements' => 'string', 'travel_address' => 'string',
'travel_distance_km' => 'number', 'travel_duration_minutes' => 'integer', 'parking_options' => 'string',
'travel_distance_km' => 'number', 'parking_options' => 'string',
'delivery_access' => 'string', 'direct_unload_possible' => 'bool', 'storage_options' => 'string',
'travel_notes' => 'string', 'payment_status' => 'string', 'payment_date' => 'string',
'paid_amount' => 'number', 'payment_method_code' => 'string', 'cancellation_terms' => 'string',
@@ -798,6 +932,7 @@ function market_management_create_occurrence(PDO $pdo, array $input): array
);
market_management_execute_typed($stmt, $params);
$occurrence = (array) $stmt->fetch();
market_management_checklist_initialize($pdo, (int) $occurrence['id']);
foreach ($days as $day) {
if (!is_array($day)) {
throw new InvalidArgumentException('Jeder Markttag muss ein Objekt sein.');
@@ -905,6 +1040,45 @@ function market_management_get_occurrence(PDO $pdo, int $occurrenceId, bool $inc
$occurrenceSalesCount += (int) $salesRow['sales_count'];
}
unset($day);
$salesItemsStmt = $pdo->prepare(
'SELECT so.market_occurrence_day_id AS day_id,
d.date,
p.id AS product_id,
p.sku,
p.name AS product_name,
SUM(GREATEST(sol.qty - sol.qty_cancelled, 0) * sic.qty_per_item) AS quantity,
SUM(CASE WHEN sol.line_status = \'cancelled\' OR sol.line_total IS NULL THEN 0
ELSE sol.line_total * GREATEST(sol.qty - sol.qty_cancelled, 0) / NULLIF(sol.qty, 0) END) AS gross_revenue
FROM sales_order so
INNER JOIN sales_order_line sol ON sol.sales_order_id = so.id
INNER JOIN sellable_item_component sic ON sic.sellable_item_id = sol.sellable_item_id
INNER JOIN product p ON p.id = sic.product_id
INNER JOIN market_occurrence_day d ON d.id = so.market_occurrence_day_id
WHERE so.market_occurrence_id = :occurrence_id
AND so.market_occurrence_day_id IS NOT NULL
AND so.order_source = \'direct\'
AND so.order_status <> \'cancelled\'
GROUP BY so.market_occurrence_day_id, d.date, p.id, p.sku, p.name
ORDER BY d.date ASC, p.sku ASC, p.id ASC'
);
$salesItemsStmt->execute([':occurrence_id' => $occurrenceId]);
$occurrence['sales_items'] = array_map(
static function (array $item): array {
$quantity = (float) $item['quantity'];
$grossRevenue = (float) $item['gross_revenue'];
return [
'day_id' => (int) $item['day_id'],
'date' => (string) $item['date'],
'product_id' => (int) $item['product_id'],
'sku' => (string) $item['sku'],
'product_name' => (string) $item['product_name'],
'quantity' => $quantity,
'gross_revenue' => $grossRevenue,
'unit_price' => $quantity > 0 ? $grossRevenue / $quantity : null,
];
},
$salesItemsStmt->fetchAll()
);
$occurrence['sales_summary'] = $occurrenceSalesCount === 0
? [
'sales_count' => 0,
@@ -999,6 +1173,7 @@ function market_management_get_occurrence(PDO $pdo, int $occurrenceId, bool $inc
? null
: round((float) $summary['gross_amount'] - (float) $occurrence['paid_amount'], 2),
];
$occurrence['checklist'] = market_management_get_occurrence_checklist($pdo, $occurrenceId, $includeArchived);
return $occurrence;
}
@@ -1014,12 +1189,12 @@ function market_management_update_occurrence(PDO $pdo, int $occurrenceId, array
}
$definitions = [
'status' => 'string', 'place' => 'string', 'address' => 'string', 'municipality_reference_id' => 'nullable_id',
'notes' => 'string', 'registration_deadline' => 'string', 'request_or_registration_date' => 'string',
'notes' => 'string', 'registration_deadline' => 'string', 'request_or_registration_date' => 'string', 'registration_note' => 'string',
'confirmation_date' => 'string', 'payment_deadline' => 'string', 'stand_size' => 'string',
'table_available' => 'bool', 'table_rentable' => 'bool', 'own_table_required' => 'bool',
'shelter_available' => 'bool', 'power_available' => 'bool', 'water_available' => 'bool',
'lighting_available' => 'bool', 'other_infrastructure' => 'string', 'own_stand_requirements' => 'string',
'travel_address' => 'string', 'travel_distance_km' => 'number', 'travel_duration_minutes' => 'integer',
'travel_address' => 'string', 'travel_distance_km' => 'number',
'parking_options' => 'string', 'delivery_access' => 'string', 'direct_unload_possible' => 'bool',
'storage_options' => 'string', 'travel_notes' => 'string', 'payment_status' => 'string',
'payment_date' => 'string', 'paid_amount' => 'number', 'payment_method_code' => 'string',
@@ -1330,7 +1505,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 +1556,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 +1595,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 +1606,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']);
+110 -41
View File
@@ -87,6 +87,20 @@ function auth_market_status_signal_class(string $status): string
][$status] ?? '';
}
function auth_large_table_default_width(int $columnCount): string
{
return rtrim(rtrim(number_format(100 / max(1, $columnCount), 4, '.', ''), '0'), '.') . '%';
}
function auth_large_table_resize_handle(string $label, int $columnIndex, int $columnCount): string
{
if ($columnIndex >= $columnCount - 1) {
return '';
}
return '<span class="sg-large-table__resize-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Breite von ' . auth_escape_html($label) . ' anpassen" data-large-table-resize-handle="' . $columnIndex . '"></span>';
}
function auth_render_markets_large_table(array $marketTable): string
{
$rows = $marketTable['rows'] ?? $marketTable;
@@ -107,7 +121,8 @@ function auth_render_markets_large_table(array $marketTable): string
$html = [];
$html[] = '<section class="sg-card-list-page" aria-label="Marktdurchführungen">';
$html[] = '<article class="sg-card" data-component="basic-card"><div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-component-part="card-body"><div class="sg-component-row sg-basic-card__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" aria-haspopup="dialog" aria-expanded="false" data-component="button" data-component-state="active" data-market-occurrence-create>Neue Marktdurchführung</button></div></div></article>';
$html[] = '<article class="sg-card sg-large-table sg-large-table--markets" data-component="large-table" data-markets-large-table="true" data-markets-search="' . auth_escape_html($search) . '" data-markets-sort-column="' . auth_escape_html($sortColumn) . '" data-markets-sort-direction="' . auth_escape_html($sortDirection) . '" data-markets-limit="' . $limit . '" data-markets-page-size="' . $pageSize . '" role="table" aria-label="Marktdurchführungen">';
$html[] = '<article class="sg-card sg-large-table sg-large-table--markets" data-component="large-table" data-column-resize="true" data-column-resize-key="market-occurrences" data-column-min-width="8rem" data-markets-large-table="true" data-markets-search="' . auth_escape_html($search) . '" data-markets-sort-column="' . auth_escape_html($sortColumn) . '" data-markets-sort-direction="' . auth_escape_html($sortDirection) . '" data-markets-limit="' . $limit . '" data-markets-page-size="' . $pageSize . '" role="table" aria-label="Marktdurchführungen">';
$html[] = '<div class="sg-large-table__viewport" data-component-part="large-table-viewport">';
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header">';
$html[] = '<div class="sg-strong">Marktdurchführungen</div>';
$html[] = '<span class="sg-search-field-row" data-component="search-field">';
@@ -123,10 +138,10 @@ function auth_render_markets_large_table(array $marketTable): string
$iconDirection = $isActive ? strtolower($sortDirection) : 'ascending';
$targetDirection = $isActive ? ($sortDirection === 'ASC' ? 'DESC' : 'ASC') : ($column === 'earliest_date' ? 'ASC' : 'ASC');
$ariaLabel = $isActive ? $label . ' ' . ($sortDirection === 'ASC' ? 'aufsteigend sortiert' : 'absteigend sortiert') : $label . ' sortieren';
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="' . $headerDirection . '" data-sort-key="' . $headerIndex . '"><button class="sg-large-table__sort-button" type="button" aria-label="' . auth_escape_html($ariaLabel) . '" data-markets-sort-button="true" data-markets-sort-column="' . auth_escape_html($column) . '" data-markets-sort-direction="' . auth_escape_html($targetDirection) . '"><span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span><span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="' . auth_escape_html($iconDirection) . '"></span></button></div>';
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="' . $headerDirection . '" data-sort-key="' . $headerIndex . '" data-default-width="' . auth_large_table_default_width(6) . '"><button class="sg-large-table__sort-button" type="button" aria-label="' . auth_escape_html($ariaLabel) . '" data-markets-sort-button="true" data-markets-sort-column="' . auth_escape_html($column) . '" data-markets-sort-direction="' . auth_escape_html($targetDirection) . '"><span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span><span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="' . auth_escape_html($iconDirection) . '"></span></button>' . auth_large_table_resize_handle($label, $headerIndex, 6) . '</div>';
$headerIndex++;
}
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader">Verkaufte Produkte</div>';
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" data-default-width="' . auth_large_table_default_width(6) . '"><span class="sg-large-table__sort-label">Verkaufte Produkte</span></div>';
$html[] = '</div>';
foreach ($rows as $index => $row) {
@@ -159,9 +174,9 @@ function auth_render_markets_large_table(array $marketTable): string
}
if ($hasMore) {
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--load-more" role="row" data-component-part="large-table-row"><div class="sg-large-table__cell sg-large-table__cell--load-more" role="cell"><div class="sg-navigation-card-layout sg-large-table__load-more-layout"><div class="sg-navigation-card-block"><article class="sg-card" data-component="card" data-pattern="navigation-card" aria-label="Mehr 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" data-markets-load-more-trigger="true">Mehr laden</a></div></div></article></div></div></div></div>';
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--load-more" role="row" data-component-part="large-table-row"><div class="sg-large-table__cell sg-large-table__cell--load-more" role="cell"><article class="sg-card sg-large-table__load-more-card" data-component="card" data-pattern="navigation-card" aria-label="Mehr 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" data-markets-load-more-trigger="true">Mehr laden</a></div></div></article></div></div>';
}
$html[] = '</article></section>';
$html[] = '</div></article></section>';
return implode('', $html);
}
@@ -180,14 +195,35 @@ function auth_render_market_definitions_content(array $marketTable): string
$rows = $marketTable['rows'] ?? $marketTable;
$categories = $marketTable['categories'] ?? [];
$organizers = $marketTable['organizers'] ?? [];
$sortColumn = (string) ($marketTable['sort_column'] ?? 'name');
$sortDirection = strtoupper((string) ($marketTable['sort_direction'] ?? 'ASC')) === 'DESC' ? 'DESC' : 'ASC';
$html = [];
$html[] = '<div data-market-definitions-content="true" data-market-definitions-csrf-token="' . auth_escape_html(auth_csrf_token()) . '"><section class="sg-card-list-page" aria-label="Märkte">';
$html[] = '<article class="sg-card" data-component="basic-card"><div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-component-part="card-body"><div class="sg-component-row sg-basic-card__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" aria-haspopup="dialog" aria-expanded="false" data-component="button" data-market-definition-create>Neuer Markt</button></div></div></article>';
$html[] = '<article class="sg-card sg-large-table sg-large-table--markets" data-component="large-table" data-market-definitions-table="true" role="table" aria-label="Märkte">';
$html[] = '<article class="sg-card sg-large-table sg-large-table--markets" data-component="large-table" data-column-resize="true" data-column-resize-key="market-definitions" data-column-min-width="8rem" data-market-definitions-table="true" role="table" aria-label="Märkte">';
$html[] = '<div class="sg-large-table__viewport" data-component-part="large-table-viewport">';
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header"><div class="sg-strong">Märkte</div></div>';
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--header" role="row" data-component-part="large-table-header-row">';
foreach (['Markt', 'Kategorie', 'Veranstalter', 'Website', 'Durchführungen', 'Status'] as $label) {
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader">' . auth_escape_html($label) . '</div>';
$definitionColumns = [
'name' => 'Markt',
'category_name' => 'Kategorie',
'organizer_name' => 'Veranstalter',
'website_url' => 'Website',
'occurrence_count' => 'Durchführungen',
'status' => 'Status',
];
$definitionColumnIndex = 0;
foreach ($definitionColumns as $column => $label) {
$isActive = $column === $sortColumn;
$headerDirection = $isActive ? strtolower($sortDirection) : 'none';
$iconDirection = $isActive && $sortDirection === 'DESC' ? 'descending' : 'ascending';
$targetDirection = $isActive && $sortDirection === 'ASC' ? 'DESC' : 'ASC';
$ariaLabel = $isActive
? $label . ' ' . ($sortDirection === 'ASC' ? 'aufsteigend sortiert' : 'absteigend sortiert')
: $label . ' sortieren';
$sortButton = '<button class="sg-large-table__sort-button" type="button" aria-label="' . auth_escape_html($ariaLabel) . '" data-market-definition-sort data-market-definition-sort-column="' . auth_escape_html($column) . '" data-market-definition-sort-direction="' . auth_escape_html($targetDirection) . '"><span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span><span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="' . auth_escape_html($iconDirection) . '"></span></button>';
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="' . auth_escape_html($headerDirection) . '" data-sort-key="' . $definitionColumnIndex . '" data-default-width="' . auth_large_table_default_width(count($definitionColumns)) . '">' . $sortButton . auth_large_table_resize_handle($label, $definitionColumnIndex, count($definitionColumns)) . '</div>';
$definitionColumnIndex++;
}
$html[] = '</div>';
foreach ($rows as $index => $row) {
@@ -210,7 +246,7 @@ function auth_render_market_definitions_content(array $marketTable): string
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html($status) . '</div>';
$html[] = '</div>';
}
$html[] = '</article></section>';
$html[] = '</div></article></section>';
$html[] = '<div class="sg-card-list-page-drawer-backdrop" aria-hidden="true" data-market-definition-drawer-backdrop="true" data-open="false"></div>';
$html[] = '<aside class="sg-card-list-page-drawer" aria-label="Markt-Details" aria-hidden="true" data-market-definition-drawer="true" data-open="false"><div class="sg-card-list-page-drawer__content" data-market-definition-drawer-content><article class="sg-card sg-card--content-card" data-component="content-card" data-shared-editable-action-card data-market-definition-card><header class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen sg-object-card__header"><h2 class="sg-strong sg-text-on-dark" data-market-definition-read-title>Markt</h2><div class="sg-component-row"><button class="sg-interaction-element sg-button sg-button--active" type="button" data-market-definition-edit data-shared-editable-card-edit>Bearbeiten</button></div></header><div class="sg-card-segment sg-card-segment--body sg-card-segment--white" data-market-definition-read-card data-shared-editable-card-read><dl class="market-detail-grid"><div class="market-detail-row"><dt>Marktname</dt><dd data-market-definition-read-name></dd></div><div class="market-detail-row"><dt>Kategorie/Marktart</dt><dd data-market-definition-read-category></dd></div><div class="market-detail-row"><dt>Veranstalter</dt><dd data-market-definition-read-organizer></dd></div><div class="market-detail-row"><dt>Website</dt><dd data-market-definition-read-website></dd></div></dl></div><div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-market-definition-create-card data-shared-editable-card-form hidden><h2 class="sg-strong" data-market-definition-form-title>Markt bearbeiten</h2><div class="sg-form-sections-card-wrapper" data-pattern="form-sections"><form class="sg-form-sections-card" data-market-definition-form><div class="sg-form-sections-card__body" data-pattern-part="form-body"><div class="sg-form-sections-card__section"><div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Marktname<span class="market-required-field-indicator" aria-hidden="true">*</span></span><input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" name="name" required data-market-definition-name></label></div><div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Kategorie/Marktart<span class="market-required-field-indicator" aria-hidden="true">*</span></span><select class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable sg-pulldown" name="category_id" required data-market-definition-category><option value="">Bitte auswählen</option>';
foreach ($categories as $category) {
@@ -244,7 +280,8 @@ function auth_render_bestellungen_large_table(array $bestellungenTable): string
$html = [];
$html[] = '<section class="sg-card-list-page" aria-label="Bestellungen">';
$html[] = '<article class="sg-card sg-large-table sg-large-table--bestellungen" style="margin-top: var(--spacing-large);" data-component="large-table" data-bestellungen-large-table="true" data-bestellungen-search="' . auth_escape_html($search) . '" data-bestellungen-sort-column="' . auth_escape_html($sortColumn) . '" data-bestellungen-sort-direction="' . auth_escape_html($sortDirection) . '" data-bestellungen-limit="' . $limit . '" data-bestellungen-page-size="' . $pageSize . '" role="table" aria-label="Bestellungen">';
$html[] = '<article class="sg-card sg-large-table sg-large-table--bestellungen" style="margin-top: var(--spacing-large);" data-component="large-table" data-column-resize="true" data-column-resize-key="orders" data-column-min-width="8rem" data-bestellungen-large-table="true" data-bestellungen-search="' . auth_escape_html($search) . '" data-bestellungen-sort-column="' . auth_escape_html($sortColumn) . '" data-bestellungen-sort-direction="' . auth_escape_html($sortDirection) . '" data-bestellungen-limit="' . $limit . '" data-bestellungen-page-size="' . $pageSize . '" role="table" aria-label="Bestellungen">';
$html[] = '<div class="sg-large-table__viewport" data-component-part="large-table-viewport">';
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header">';
$html[] = '<div class="sg-strong">Bestellungen</div>';
$html[] = '<span class="sg-search-field-row" data-component="search-field">';
@@ -269,11 +306,11 @@ function auth_render_bestellungen_large_table(array $bestellungenTable): string
: ($column === 'order_date' ? 'DESC' : 'ASC');
$amountHeaderClass = $column === 'total_amount' ? ' sg-large-table__cell--amount' : '';
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header' . $amountHeaderClass . '" role="columnheader" aria-sort="' . $headerDirection . '" data-sort-key="' . $headerIndex . '">';
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header' . $amountHeaderClass . '" role="columnheader" aria-sort="' . $headerDirection . '" data-sort-key="' . $headerIndex . '" data-default-width="' . auth_large_table_default_width(4) . '">';
$html[] = '<button class="sg-large-table__sort-button" type="button" aria-label="' . auth_escape_html($ariaLabel) . '" data-bestellungen-sort-button="true" data-bestellungen-sort-column="' . auth_escape_html($column) . '" data-bestellungen-sort-direction="' . auth_escape_html($targetDirection) . '">';
$html[] = '<span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span>';
$html[] = '<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="' . auth_escape_html($iconDirection) . '"></span>';
$html[] = '</button>';
$html[] = '</button>' . auth_large_table_resize_handle($label, $headerIndex, 4);
$html[] = '</div>';
$headerIndex++;
}
@@ -321,9 +358,7 @@ function auth_render_bestellungen_large_table(array $bestellungenTable): string
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--load-more" role="row" data-component-part="large-table-row" data-large-table-load-more-row="true">';
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--load-more" role="cell">';
$html[] = '<div class="sg-navigation-card-layout sg-large-table__load-more-layout">';
$html[] = '<div class="sg-navigation-card-block">';
$html[] = '<article class="sg-card" data-component="card" data-pattern="navigation-card" aria-label="Mehr laden">';
$html[] = '<article class="sg-card sg-large-table__load-more-card" data-component="card" data-pattern="navigation-card" aria-label="Mehr laden">';
$html[] = '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment">';
$html[] = '<div class="sg-navigation-card-center">';
$html[] = '<a class="sg-hyperlink" href="' . auth_escape_html($loadMoreUrl) . '" data-component="hyperlink" data-large-table-load-more-trigger="true">Mehr laden</a>';
@@ -331,11 +366,9 @@ function auth_render_bestellungen_large_table(array $bestellungenTable): string
$html[] = '</div>';
$html[] = '</article>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</article>';
$html[] = '</section>';
@@ -388,11 +421,12 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
return ['text' => $text, 'warning' => $warning, 'mhdWarning' => $mhdWarning];
};
$html = [];
$html[] = '<article class="sg-card sg-large-table sg-large-table--lager" data-component="large-table" data-lager-table="true" data-lager-table-key="' . auth_escape_html($tableKey) . '" role="table" aria-label="' . auth_escape_html($title) . '">';
$html[] = '<article class="sg-card sg-large-table sg-large-table--lager" data-component="large-table" data-column-resize="true" data-column-resize-key="lager-' . auth_escape_html($tableKey) . '" data-column-min-width="8rem" data-lager-table="true" data-lager-table-key="' . auth_escape_html($tableKey) . '" role="table" aria-label="' . auth_escape_html($title) . '">';
$html[] = '<div class="sg-large-table__viewport" data-component-part="large-table-viewport">';
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header"><div class="sg-strong">' . auth_escape_html($title) . '</div></div>';
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--header" role="row" data-component-part="large-table-header-row">';
foreach ($columns as $label) {
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader"><span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span></div>';
foreach ($columns as $index => $label) {
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" data-default-width="' . auth_large_table_default_width(count($columns)) . '"><span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span>' . auth_large_table_resize_handle($label, $index, count($columns)) . '</div>';
}
$html[] = '</div>';
@@ -474,6 +508,7 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</article>';
return implode('', $html);
}
@@ -739,7 +774,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
echo '<meta charset="UTF-8">';
echo '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
echo '<title>Naurua ERP</title>';
echo '<link rel="stylesheet" href="/assets/styles.css">';
echo '<link rel="stylesheet" href="/assets/styles.css?v=' . filemtime(__DIR__ . '/../../../../public/assets/styles.css') . '">';
echo '<link rel="stylesheet" href="/assets/styles.portal.css?v=' . filemtime(__DIR__ . '/../../../../public/assets/styles.portal.css') . '">';
echo '</head>';
echo '<body>';
@@ -833,7 +868,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
echo '</select>';
echo '</label>';
echo '<label class="sg-labeled-input-row"><span class="sg-label">Betrag (CHF)</span>';
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="number" id="otc-payment-amount-1" min="0.01" step="0.01" value="0.00" data-otc-payment-amount>';
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="number" id="otc-payment-amount-1" min="0.01" step="0.01" placeholder="0.00" data-otc-payment-amount>';
echo '</label>';
echo '</div>';
echo '</div>';
@@ -930,11 +965,15 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
echo '</div>';
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-editable-action-card.js?v=20260930-2"></script>';
echo '<script src="/scripts/shared-mobile-detail.js?v=20260925-1"></script>';
echo '<script src="/scripts/large-table-column-resize.js?v=' . filemtime(__DIR__ . '/../../../../public/scripts/large-table-column-resize.js') . '"></script>';
echo '<script>';
echo 'const portalModuleNavigation = ' . $moduleNavigation . ';';
echo 'const portalModuleContentCards = ' . $moduleContentCards . ';';
echo 'const portalModuleHeadings = ' . $moduleHeadings . ';';
echo "const portalContentBody = document.querySelector('[data-left-navigation-content-body]');";
echo "if (portalContentBody && window.sgInitLargeTableColumnResize) { const portalLargeTableResizeObserver = new MutationObserver(() => window.sgInitLargeTableColumnResize({ root: portalContentBody })); portalLargeTableResizeObserver.observe(portalContentBody, { childList: true }); window.sgInitLargeTableColumnResize({ root: portalContentBody }); }";
echo "const renderMainHeading = (moduleName) => {";
echo " const heading = document.querySelector('[data-main-heading]');";
echo " if (heading && portalModuleHeadings[moduleName]) { heading.textContent = portalModuleHeadings[moduleName]; }";
@@ -1071,15 +1110,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,17 +1191,25 @@ 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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[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(/&lt;br&gt;/g, '<br>').replace(/&lt;strong&gt;/g, '<strong>').replace(/&lt;\\/strong&gt;/g, '</strong>').replace(/&lt;a class=&quot;sg-hyperlink&quot; href=&quot;(https?:\\/\\/.*?)&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;/g, '<a class=\"sg-hyperlink\" href=\"$1\" target=\"_blank\" rel=\"noopener noreferrer\">').replace(/&lt;\\/a&gt;/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(' ')); };";
echo " const marketDate = (value) => { if (!value) { return 'Nicht hinterlegt'; } const parts = String(value).split('-'); return parts.length === 3 ? parts[2] + '.' + parts[1] + '.' + parts[0] : marketEscape(value); };";
echo " const marketStatusLabel = (value) => ({ open: 'Offen', waitlist: 'Warteliste', registered: 'Angemeldet', confirmation_pending: 'Bestätigung ausstehend', confirmed: 'Bestätigt', cancelled: 'Abgesagt', completed: 'Abgeschlossen' }[value] || value || 'Nicht hinterlegt');";
echo " const normalizeMarketBooleanFields = (fields) => { ['table_available', 'table_rentable', 'own_table_required', 'shelter_available', 'power_available', 'water_available', 'lighting_available', 'direct_unload_possible'].forEach((field) => { if (fields[field] === '') { fields[field] = null; } }); return fields; };";
echo " const marketStatusSegment = (value) => ({ waitlist: 'sg-card-segment--signal-red', registered: 'sg-card-segment--signal-yellow', confirmation_pending: 'sg-card-segment--signal-yellow', confirmed: 'sg-card-segment--signal-green' }[value] || 'sg-card-segment--gray');";
echo " const marketChecklistSignalSegment = (items) => { const list = Array.isArray(items) ? items : []; const completed = list.filter((item) => item && item.is_completed).length; return completed === 0 ? 'sg-card-segment--signal-red' : completed === list.length ? 'sg-card-segment--signal-green' : 'sg-card-segment--signal-yellow'; };";
echo " const marketRow = (label, value) => '<div class=\"market-detail-row\"><dt>' + marketEscape(label) + '</dt><dd>' + marketDisplay(value) + '</dd></div>';";
echo " const initializeMunicipalityFields = (scope) => { scope.querySelectorAll('select[name=\"municipality_reference_id\"]').forEach((select) => { if (!select.querySelector('option[value=\"__new__\"]')) { const option = document.createElement('option'); option.value = '__new__'; option.textContent = 'Neue Gemeinde'; select.append(option); } const form = select.closest('form'); if (!form) { return; } let fields = form.querySelector('[data-market-new-municipality]'); if (!fields) { fields = document.createElement('div'); fields.className = 'sg-form-sections-card__field-group'; fields.dataset.marketNewMunicipality = ''; fields.hidden = true; fields.innerHTML = '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Gemeinde</span><input class=\"sg-interaction-element sg-input-single-line\" type=\"text\" name=\"municipality_name\" data-market-new-municipality-name></label>'; select.closest('.sg-form-sections-card__field-group')?.after(fields); } const sync = () => { const visible = select.value === '__new__'; fields.hidden = !visible; const input = fields.querySelector('[data-market-new-municipality-name]'); if (input) { input.disabled = !visible; input.setAttribute('aria-disabled', String(!visible)); } }; if (select.dataset.municipalityBound !== 'true') { select.addEventListener('change', sync); select.dataset.municipalityBound = 'true'; } sync(); }); };";
echo " const syncMunicipalityReadValue = (scope) => { const drawer = scope.querySelector('[data-market-drawer]'); if (!drawer?.dataset.marketPayload) { return; } let payload = {}; try { payload = JSON.parse(drawer.dataset.marketPayload); } catch (error) { return; } const value = payload.occurrence?.municipality_name || 'Nicht hinterlegt'; scope.querySelectorAll('.market-detail-row').forEach((row) => { if (row.querySelector('dt')?.textContent?.trim() !== 'Gemeinde') { return; } const output = row.querySelector('dd'); if (output && output.textContent !== value) { output.textContent = value; } }); };";
echo " const marketCard = (title, body, headerClass = 'sg-card-segment--darkgreen', actions = '', scope = '', id = '', values = null) => { const header = '<header class=\"sg-card-segment sg-card-segment--header ' + headerClass + ' sg-object-card__header\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">' + title + '</div>' + (actions ? '<div class=\"sg-component-row\">' + actions + '</div>' : '') + '</header>'; return '<article class=\"sg-card sg-card--content-card market-detail-card\" data-component=\"content-card\"' + (scope ? ' data-shared-editable-action-card' : '') + '>' + header + '<div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\"' + (scope ? ' data-shared-editable-card-read' : '') + '>' + body + '</div>' + (scope ? '<div class=\"sg-card-segment sg-card-segment--body sg-card-segment--gray\" data-shared-editable-card-form hidden>' + marketEditForm(scope, id, values) + '</div>' : '') + '</article>'; };";
echo " const marketChecklistChapters = (checklist) => Array.isArray(checklist.chapters) ? checklist.chapters : [];";
echo " const marketChecklistReadBody = (checklist) => { const items = Array.isArray(checklist.items) ? checklist.items : []; const chapterSections = marketChecklistChapters(checklist).map((chapter) => { const chapterItems = items.filter((item) => item.chapter_key === chapter.key); const rows = chapterItems.map((item) => { const index = items.indexOf(item); return '<div class=\"market-checklist-item\"><button class=\"sg-checkbox-field sg-checkbox-field--on-grey' + (item.is_completed ? ' sg-form-active' : '') + '\" type=\"button\" role=\"checkbox\" aria-checked=\"' + (item.is_completed ? 'true' : 'false') + '\" aria-label=\"Checklistenpunkt ' + (index + 1) + '\" data-checklist-read-checkbox=\"' + index + '\"><span class=\"sg-checkbox-field__mark\" aria-hidden=\"true\">✓</span></button><span class=\"sg-body\">' + marketEscape(item.label || '') + '</span></div>'; }).join(''); return '<section class=\"sg-form-sections-card__chapter market-checklist-chapter\" data-checklist-chapter=\"' + marketEscape(chapter.key) + '\"><h3 class=\"sg-form-sections-card__chapter-title\">' + marketEscape(chapter.label) + '</h3><div class=\"market-checklist-list\">' + rows + '</div></section>'; }).join(''); return '<div class=\"market-checklist-chapters\">' + chapterSections + '</div><div class=\"market-checklist-comment\"><div class=\"sg-strong\">Kommentar</div><p class=\"sg-body\">' + marketDisplay(checklist.comment) + '</p></div><p class=\"sg-body\" data-checklist-read-status aria-live=\"polite\"></p>'; };";
echo " const marketChecklistEditBody = (checklist) => { const items = Array.isArray(checklist.items) ? checklist.items : []; const sections = marketChecklistChapters(checklist).map((chapter) => { const chapterItems = items.filter((item) => item.chapter_key === chapter.key); const rows = chapterItems.map((item) => '<label class=\"sg-labeled-input-row market-checklist-edit-row\" data-checklist-edit-item data-checklist-chapter-key=\"' + marketEscape(chapter.key) + '\"><span class=\"sg-label\"><button class=\"sg-checkbox-field sg-checkbox-field--on-grey' + (item.is_completed ? ' sg-form-active' : '') + '\" type=\"button\" role=\"checkbox\" aria-checked=\"' + (item.is_completed ? 'true' : 'false') + '\" aria-label=\"Checklistenpunkt abhaken\" data-checklist-edit-checkbox><span class=\"sg-checkbox-field__mark\" aria-hidden=\"true\">✓</span></button></span><span class=\"market-checklist-edit-control\"><input class=\"sg-interaction-element sg-input-single-line\" type=\"text\" value=\"' + marketEscape(item.label || '') + '\" data-checklist-label><button class=\"sg-interaction-element sg-button sg-button--warning market-checklist-delete\" type=\"button\" aria-label=\"Checklistenpunkt löschen\" data-checklist-delete-item>−</button></span></label>').join(''); const trailing = '<label class=\"sg-labeled-input-row market-checklist-edit-row market-checklist-edit-row--trailing\" data-checklist-edit-item data-checklist-trailing-item data-checklist-chapter-key=\"' + marketEscape(chapter.key) + '\"><span class=\"sg-label\"><button class=\"sg-checkbox-field sg-checkbox-field--on-grey\" type=\"button\" role=\"checkbox\" aria-checked=\"false\" aria-label=\"Neuen Checklistenpunkt abhaken\" data-checklist-edit-checkbox><span class=\"sg-checkbox-field__mark\" aria-hidden=\"true\">✓</span></button></span><span class=\"market-checklist-edit-control\"><input class=\"sg-interaction-element sg-input-single-line\" type=\"text\" value=\"\" aria-label=\"Neuer Checklistenpunkt\" data-checklist-label></span></label>'; return '<section class=\"sg-form-sections-card__chapter market-checklist-chapter\" data-checklist-chapter=\"' + marketEscape(chapter.key) + '\"><h3 class=\"sg-form-sections-card__chapter-title\">' + marketEscape(chapter.label) + '</h3><div class=\"market-checklist-edit-list\" data-checklist-edit-list data-checklist-chapter-key=\"' + marketEscape(chapter.key) + '\">' + rows + trailing + '</div></section>'; }).join(''); return sections + '<label class=\"sg-labeled-input-row market-checklist-comment-field\"><span class=\"sg-label\">Kommentar</span><textarea class=\"sg-interaction-element sg-input-multi-line\" name=\"comment\" rows=\"4\" data-checklist-comment>' + marketEscape(checklist.comment || '') + '</textarea></label>'; };";
echo " const marketChecklistCard = (id, checklist) => { const headerClass = marketChecklistSignalSegment(checklist.items); const actions = '<button class=\"sg-interaction-element sg-button sg-button--active\" type=\"button\" data-shared-editable-card-toggle aria-expanded=\"false\">Anzeigen</button>' + marketEditButton('checklist', id); return '<article class=\"sg-card sg-card--content-card market-detail-card\" data-component=\"content-card\" data-shared-editable-action-card data-shared-editable-card-collapsible data-shared-editable-card-collapsed data-checklist-card><header class=\"sg-card-segment sg-card-segment--header ' + headerClass + ' sg-object-card__header\"><div class=\"sg-strong\">Checkliste</div><div class=\"sg-component-row\">' + actions + '</div></header><div class=\"sg-card-segment sg-card-segment--body sg-card-segment--gray\" data-shared-editable-card-read hidden>' + marketChecklistReadBody(checklist) + '</div><div class=\"sg-card-segment sg-card-segment--body sg-card-segment--gray\" data-shared-editable-card-form hidden><form class=\"sg-form-sections-card market-checklist-form\" data-market-checklist-form data-market-checklist-id=\"' + marketEscape(id) + '\"><div class=\"sg-form-sections-card__body\" data-pattern-part=\"form-body\">' + marketChecklistEditBody(checklist) + '</div><footer class=\"sg-form-sections-card__actions-segment\" data-pattern-part=\"form-actions-segment\"><div class=\"sg-form-sections-card__actions\" data-pattern-part=\"form-actions\"><button class=\"sg-interaction-element sg-button sg-button--active sg-form-sections-card__action\" type=\"button\" data-shared-editable-card-cancel>Abbrechen</button><button class=\"sg-interaction-element sg-button sg-button--process sg-form-sections-card__action\" type=\"submit\">Speichern</button></div></footer><p class=\"sg-body\" data-market-checklist-status aria-live=\"polite\"></p></form></div></article>'; };";
echo " const setChecklistVisibility = (card, visible, showRead = visible) => { if (!window.SharedEditableActionCard) { return; } window.SharedEditableActionCard.setVisibility(card, visible); if (!showRead) { const readView = card?.querySelector('[data-shared-editable-card-read]'); if (readView) { readView.hidden = true; } } };";
echo " const setChecklistSignal = (card, items) => { const header = card?.querySelector('header'); if (!header) { return; } header.classList.remove('sg-card-segment--signal-red', 'sg-card-segment--signal-yellow', 'sg-card-segment--signal-green'); header.classList.add(marketChecklistSignalSegment(items)); };";
echo " const marketStatusCard = (id, values) => { const headerClass = marketStatusSegment(values.status); const textClass = headerClass === 'sg-card-segment--gray' ? '' : 'sg-text-on-dark'; const actions = marketEditButton('status', id); const header = '<header class=\"sg-card-segment sg-card-segment--header ' + headerClass + ' sg-object-card__header\" data-component-part=\"card-header\"><div class=\"sg-strong ' + textClass + '\">Status: ' + marketEscape(marketStatusLabel(values.status)) + '</div><div class=\"sg-component-row\">' + actions + '</div></header>'; return '<article class=\"sg-card sg-card--content-card market-detail-card\" data-component=\"content-card\" data-shared-editable-action-card>' + header + '<div class=\"sg-card-segment sg-card-segment--body sg-card-segment--gray\" data-shared-editable-card-form hidden>' + marketEditForm('status', id, values) + '</div></article>'; };";
echo " const marketEditButton = () => '<button class=\"sg-interaction-element sg-button sg-button--active\" type=\"button\" data-shared-editable-card-edit>Bearbeiten</button>';";
echo " const marketHumanizeError = (error) => { const message = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'; const labels = { table_available: 'Tisch vorhanden', table_rentable: 'Tisch mietbar', own_table_required: 'Eigener Tisch erforderlich', shelter_available: 'Überdachung', power_available: 'Strom', water_available: 'Wasser', lighting_available: 'Beleuchtung', direct_unload_possible: 'Direktes Entladen' }; const match = message.match(/^([a-z_]+) muss boolean sein\\.$/); return match && labels[match[1]] ? labels[match[1]] + ' muss Ja oder Nein sein.' : message; };";
@@ -1171,18 +1218,19 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
echo " const marketEditField = (key, label, type, value, options = [], required = false, minDate = '') => { const current = value === null || value === undefined ? '' : String(value); const requiredAttributes = required ? ' required aria-required=\"true\"' : ''; const minAttribute = type === 'date' && minDate ? ' min=\"' + marketEscape(minDate) + '\"' : ''; const disabledAttributes = ' disabled aria-disabled=\"true\"'; const control = type === 'readonly' ? '<input class=\"sg-interaction-element sg-input-single-line sg-input-single-line--disabled sg-form-disabled\" type=\"text\" name=\"' + key + '\" value=\"' + marketEscape(current) + '\"' + disabledAttributes + '>' : type === 'readonly-textarea' ? '<textarea class=\"sg-input-multi-line sg-form-disabled\" name=\"' + key + '\" rows=\"4\"' + disabledAttributes + '>' + marketEscape(current) + '</textarea>' : type === 'textarea' ? '<textarea class=\"sg-interaction-element sg-input-multi-line\" name=\"' + key + '\" rows=\"4\"' + requiredAttributes + '>' + marketEscape(current) + '</textarea>' : type === 'select' ? '<select class=\"sg-interaction-element sg-input-single-line\" name=\"' + key + '\"' + requiredAttributes + '>' + options.map(([optionValue, optionLabel, contactEmail = '', contactPhone = '']) => '<option value=\"' + marketEscape(optionValue) + '\" data-contact-email=\"' + marketEscape(contactEmail) + '\" data-contact-phone=\"' + marketEscape(contactPhone) + '\"' + (String(optionValue) === current ? ' selected' : '') + '>' + marketEscape(optionLabel) + '</option>').join('') + '</select>' : '<input class=\"sg-interaction-element sg-input-single-line\" type=\"' + type + '\" name=\"' + key + '\" value=\"' + marketEscape(current) + '\"' + requiredAttributes + minAttribute + '>'; const requiredIndicator = required ? '<span class=\"market-required-field-indicator\" aria-hidden=\"true\">*</span>' : ''; return '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">' + marketEscape(label) + requiredIndicator + '</span>' + control + '</label>'; };";
echo " const marketDeletePattern = (id, body) => '<div class=\"sg-delete-confirmation-pattern__stage sg-delete-confirmation-pattern__host\" data-pattern=\"overlay-card\" data-dialog-open=\"false\"><div class=\"sg-delete-confirmation-pattern__target\" data-shared-editable-action-card data-shared-editable-delete-token=\"DELETE\" data-market-delete-occurrence-id=\"' + marketEscape(id) + '\"><div class=\"sg-component-row\"><button class=\"sg-interaction-element sg-button sg-button--warning\" type=\"button\" data-component=\"button\" data-component-variant=\"warning\" data-shared-editable-card-delete data-market-delete-button>Löschen</button></div>' + body + '<article class=\"sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card\" aria-label=\"Löschbestätigung\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"market-delete-title-' + marketEscape(id) + '\" data-overlay-dialog=\"delete\" data-shared-editable-card-delete-confirmation hidden><div class=\"sg-card-segment sg-card-segment--body sg-delete-confirmation-pattern__body\"><p class=\"sg-body sg-delete-confirmation-pattern__text\" id=\"market-delete-title-' + marketEscape(id) + '\"><strong>Möchtest du diese Marktdurchführung wirklich löschen?</strong></p><p class=\"sg-body sg-delete-confirmation-pattern__text\">Du kannst das nicht rückgängig machen. Bestätige durch Eingabe von <span class=\"sg-delete-confirmation-pattern__code\">DELETE</span>.</p><label class=\"sg-labeled-input-row sg-delete-confirmation-pattern__input-row\"><span class=\"sg-label\">Bestätigung</span><input class=\"sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable\" type=\"text\" placeholder=\"DELETE\" aria-label=\"Löschbestätigung durch DELETE\" data-shared-editable-card-delete-input></label><p class=\"sg-body\" data-market-delete-status aria-live=\"polite\"></p><div class=\"sg-delete-confirmation-pattern__actions\"><button class=\"sg-interaction-element sg-button sg-button--active\" type=\"button\" data-shared-editable-card-delete-cancel>Abbrechen</button><button class=\"sg-interaction-element sg-button sg-button--process sg-button--process-inactive\" type=\"button\" disabled aria-disabled=\"true\" data-shared-editable-card-delete-submit data-market-delete-submit>Löschen</button></div></div></article></div></div>' ;";
echo " const marketDeletePatternWithTitle = (id, title, body) => { const template = document.createElement('template'); template.innerHTML = marketDeletePattern(id, body); const target = template.content.querySelector('[data-market-delete-occurrence-id]'); const row = target?.querySelector(':scope > .sg-component-row'); if (target && row) { const card = document.createElement('article'); card.className = 'sg-card sg-card--content-card market-delete-confirmation-card'; const header = document.createElement('header'); header.className = 'sg-card-segment sg-card-segment--header sg-card-segment--gray sg-object-card__header'; const heading = document.createElement('h2'); heading.className = 'sg-heading-h2 market-delete-confirmation-card__title'; heading.textContent = title; header.append(heading, row); card.append(header); target.prepend(card); } return template.innerHTML; };";
echo " const marketEditForm = (scope, id, values) => { const boolOptions = [['', 'Nicht hinterlegt'], ['1', 'Ja'], ['0', 'Nein']]; const statusOptions = [['open', 'Offen'], ['waitlist', 'Warteliste'], ['registered', 'Angemeldet'], ['confirmation_pending', 'Bestätigung ausstehend'], ['confirmed', 'Bestätigt'], ['cancelled', 'Abgesagt'], ['completed', 'Abgeschlossen']]; const referenceOptions = values.reference_options || {}; const categoryOptions = [['', 'Nicht hinterlegt'], ...(Array.isArray(referenceOptions.categories) ? referenceOptions.categories.map((option) => [option.id, option.name]) : [])]; const municipalityOptions = [['', 'Nicht hinterlegt'], ...(Array.isArray(referenceOptions.municipalities) ? referenceOptions.municipalities.map((option) => [option.id, option.name]) : [])]; const partyOptions = [['', 'Nicht hinterlegt'], ...(Array.isArray(referenceOptions.parties) ? referenceOptions.parties.map((option) => [option.id, option.name]) : [])]; const contactOptions = [['', 'Nicht hinterlegt'], ...(Array.isArray(referenceOptions.contacts) ? referenceOptions.contacts.map((option) => [option.id, [[option.first_name, option.last_name].filter(Boolean).join(' ') || 'Nicht hinterlegt', option.party_name].filter(Boolean).join(' – '), option.email || '', option.phone || '']) : [])]; const costSummary = values.cost_summary || {}; const formValues = Object.assign({}, values, { gross_cost: marketMoney(costSummary.gross_amount), net_cost: marketMoney(costSummary.net_amount), tax_cost: marketMoney(costSummary.tax_amount), open_cost: marketMoney(costSummary.open_amount), cost_items_text: values.cost_items_text, gross_revenue_display: marketMoney(values.gross_revenue), sold_item_count_display: marketDisplay(values.sold_item_count) }); const fieldSets = { status: [['status', 'Status', 'select', statusOptions]], overview: [['market_name', 'Markt', 'readonly'], ['category_name', 'Kategorie/Marktart', 'readonly'], ['website_url', 'Website', 'readonly'], ['place', 'Ort', 'text'], ['address', 'Adresse', 'textarea'], ['municipality_reference_id', 'Gemeinde', 'select', municipalityOptions], ['organization_contact_id', 'Veranstalter', 'select', partyOptions], ['contact_person_id', 'Kontaktperson', 'select', contactOptions], ['contact_email', 'Kontakt E-Mail', 'email'], ['contact_phone', 'Kontakt Telefon', 'tel']], participation: [['request_or_registration_date', 'Anmeldung / Anfrage', 'date'], ['registration_deadline', 'Anmeldefrist', 'date'], ['confirmation_date', 'Bestätigung', 'date'], ['payment_deadline', 'Zahlungsfrist', 'date'], ['payment_status', 'Zahlungsstatus', 'select', [['', 'Nicht hinterlegt'], ['paid', 'Bezahlt'], ['unpaid', 'Unbezahlt']]], ['payment_date', 'Zahlungsdatum', 'date'], ['paid_amount', 'Bezahlter Betrag', 'number'], ['payment_method_code', 'Zahlungsart', 'text'], ['gross_cost', 'Gesamtkosten brutto', 'readonly'], ['net_cost', 'Gesamtkosten netto', 'readonly'], ['tax_cost', 'Steuern', 'readonly'], ['open_cost', 'Offener Betrag', 'readonly'], ['cost_items_text', 'Kostenpositionen', 'readonly-textarea']], infrastructure: [['stand_size', 'Standgrösse', 'text'], ['table_available', 'Tisch vorhanden', 'select', boolOptions], ['table_rentable', 'Tisch mietbar', 'select', boolOptions], ['own_table_required', 'Eigener Tisch erforderlich', 'select', boolOptions], ['shelter_available', 'Überdachung', 'select', boolOptions], ['power_available', 'Strom', 'select', boolOptions], ['water_available', 'Wasser', 'select', boolOptions], ['lighting_available', 'Beleuchtung', 'select', boolOptions], ['other_infrastructure', 'Weitere Infrastruktur', 'textarea'], ['own_stand_requirements', 'Anforderungen an den eigenen Stand', 'textarea']], travel: [['travel_address', 'Reiseadresse', 'textarea'], ['travel_distance_km', 'Distanz (km)', 'number'], ['travel_duration_minutes', 'Reisedauer (Minuten)', 'number'], ['parking_options', 'Parkmöglichkeiten', 'textarea'], ['delivery_access', 'Anlieferung', 'textarea'], ['direct_unload_possible', 'Direktes Entladen', 'select', boolOptions], ['storage_options', 'Lagerung', 'textarea'], ['travel_notes', 'Besondere Anreisehinweise', 'textarea']], notes: [['cancellation_terms', 'Rücktrittsbedingungen', 'textarea'], ['revenue_share_terms', 'Umsatzbeteiligung', 'textarea'], ['other_participation_conditions', 'Teilnahmebedingungen', 'textarea'], ['notes', 'Notizen', 'textarea'], ['contract_folder_url', 'Vertragsordner', 'url']], day: [['date', 'Datum', 'date'], ['observations', 'Beobachtungen', 'textarea'], ['stand_start_time', 'Standzeit von', 'time'], ['stand_end_time', 'Standzeit bis', 'time'], ['setup_start_time', 'Aufbau von', 'time'], ['setup_end_time', 'Aufbau bis', 'time'], ['delivery_start_time', 'Anlieferung von', 'time'], ['delivery_end_time', 'Anlieferung bis', 'time'], ['gross_revenue_display', 'Umsatz', 'readonly'], ['sold_item_count_display', 'Verkaufte Artikel', 'readonly']] }; const requiredFields = { status: new Set(['status']), overview: new Set(), day: new Line truncated
echo " const marketEditForm = (scope, id, values) => { const boolOptions = [['', 'Nicht hinterlegt'], ['1', 'Ja'], ['0', 'Nein']]; const statusOptions = [['open', 'Offen'], ['waitlist', 'Warteliste'], ['registered', 'Angemeldet'], ['confirmation_pending', 'Bestätigung ausstehend'], ['confirmed', 'Bestätigt'], ['cancelled', 'Abgesagt'], ['completed', 'Abgeschlossen']]; const referenceOptions = values.reference_options || {}; const categoryOptions = [['', 'Nicht hinterlegt'], ...(Array.isArray(referenceOptions.categories) ? referenceOptions.categories.map((option) => [option.id, option.name]) : [])]; const municipalityOptions = [['', 'Nicht hinterlegt'], ...(Array.isArray(referenceOptions.municipalities) ? referenceOptions.municipalities.map((option) => [option.id, option.name]) : [])]; const partyOptions = [['', 'Nicht hinterlegt'], ...(Array.isArray(referenceOptions.parties) ? referenceOptions.parties.map((option) => [option.id, option.name]) : [])]; const contactOptions = [['', 'Nicht hinterlegt'], ...(Array.isArray(referenceOptions.contacts) ? referenceOptions.contacts.map((option) => [option.id, [[option.first_name, option.last_name].filter(Boolean).join(' ') || 'Nicht hinterlegt', option.party_name].filter(Boolean).join(' – '), option.email || '', option.phone || '']) : [])]; const costSummary = values.cost_summary || {}; const formValues = Object.assign({}, values, { gross_cost: marketMoney(costSummary.gross_amount), net_cost: marketMoney(costSummary.net_amount), tax_cost: marketMoney(costSummary.tax_amount), open_cost: marketMoney(costSummary.open_amount), cost_items_text: values.cost_items_text, gross_revenue_display: marketMoney(values.gross_revenue), sold_item_count_display: marketDisplay(values.sold_item_count) }); const fieldSets = { status: [['status', 'Status', 'select', statusOptions]], overview: [['market_name', 'Markt', 'readonly'], ['category_name', 'Kategorie/Marktart', 'readonly'], ['website_url', 'Website', 'readonly'], ['place', 'Ort', 'text'], ['address', 'Adresse', 'textarea'], ['municipality_reference_id', 'Gemeinde', 'select', municipalityOptions], ['organization_contact_id', 'Veranstalter', 'select', partyOptions], ['contact_person_id', 'Kontaktperson', 'select', contactOptions], ['contact_email', 'Kontakt E-Mail', 'email'], ['contact_phone', 'Kontakt Telefon', 'tel']], participation: [['request_or_registration_date', 'Anmeldung / Anfrage', 'date'], ['registration_note', 'Hinweis Anmeldung', 'textarea'], ['registration_deadline', 'Anmeldefrist', 'date'], ['confirmation_date', 'Bestätigung', 'date'], ['payment_deadline', 'Zahlungsfrist', 'date'], ['payment_status', 'Zahlungsstatus', 'select', [['', 'Nicht hinterlegt'], ['paid', 'Bezahlt'], ['unpaid', 'Unbezahlt']]], ['payment_date', 'Zahlungsdatum', 'date'], ['paid_amount', 'Bezahlter Betrag', 'number'], ['payment_method_code', 'Zahlungsart', 'text'], ['gross_cost', 'Gesamtkosten brutto', 'readonly'], ['net_cost', 'Gesamtkosten netto', 'readonly'], ['tax_cost', 'Steuern', 'readonly'], ['open_cost', 'Offener Betrag', 'readonly'], ['cost_items_text', 'Kostenpositionen', 'readonly-textarea']], infrastructure: [['stand_size', 'Standgrösse', 'text'], ['table_available', 'Tisch vorhanden', 'select', boolOptions], ['table_rentable', 'Tisch mietbar', 'select', boolOptions], ['own_table_required', 'Eigener Tisch erforderlich', 'select', boolOptions], ['shelter_available', 'Überdachung', 'select', boolOptions], ['power_available', 'Strom', 'select', boolOptions], ['water_available', 'Wasser', 'select', boolOptions], ['lighting_available', 'Beleuchtung', 'select', boolOptions], ['other_infrastructure', 'Weitere Infrastruktur', 'textarea'], ['own_stand_requirements', 'Anforderungen an den eigenen Stand', 'textarea']], travel: [['travel_address', 'Reiseadresse', 'textarea'], ['travel_distance_km', 'Distanz (km)', 'number'], ['parking_options', 'Parkmöglichkeiten', 'textarea'], ['delivery_access', 'Anlieferung', 'textarea'], ['direct_unload_possible', 'Direktes Entladen', 'select', boolOptions], ['storage_options', 'Lagerung', 'textarea'], ['travel_notes', 'Besondere Anreisehinweise', 'textarea']], notes: [['cancellation_terms', 'Rücktrittsbedingungen', 'textarea'], ['revenue_share_terms', 'Umsatzbeteiligung', 'textarea'], ['other_participation_conditions', 'Teilnahmebedingungen', 'textarea'], ['notes', 'Notizen', 'textarea'], ['contract_folder_url', 'Vertragsordner', 'url']], day: [['date', 'Datum', 'date'], ['observations', 'Beobachtungen', 'textarea'], ['stand_start_time', 'Standzeit von', 'time'], ['stand_end_time', 'Standzeit bis', 'time'], ['setup_start_time', 'Aufbau von', 'time'], ['setup_end_time', 'Aufbau bis', 'time'], ['delivery_start_time', 'Anlieferung von', 'time'], ['delivery_end_time', 'Anlieferung bis', 'time'], ['gross_revenue_display', 'Umsatz', 'readonly'], ['sold_item_count_display', 'Verkaufte Artikel', 'readonly']] }; const requiredFields = { status: new Set(['status']), overview: new Set(), day: new Set(['d Line truncated
echo " const marketDeletePatternWithTitleAndDays = (id, title, body) => { const html = marketDeletePatternWithTitle(id, title, body); const template = document.createElement('template'); template.innerHTML = html; const target = template.content.querySelector('[data-market-delete-occurrence-id]'); const dayGroup = target?.querySelector('.market-days-group'); if (dayGroup) { const heading = document.createElement('h2'); heading.className = 'sg-heading-h2 market-days-group__title'; heading.textContent = 'Markttage'; dayGroup.before(heading); } return template.innerHTML; };";
echo " const marketCreateForm = (marketDefinitions) => { const options = [['', 'Bitte auswählen'], ...(Array.isArray(marketDefinitions) ? marketDefinitions.map((market) => [market.id, market.name + (market.category_name ? ' – ' + market.category_name : '')]) : [])]; const fields = '<div class=\"sg-form-sections-card__field-group\">' + marketEditField('market_definition_id', 'Markt', 'select', '', options, true) + '</div><div class=\"sg-form-sections-card__field-group\">' + marketEditField('date', 'Erster Markttag', 'date', '', [], true) + '</div>'; return '<article class=\"sg-card sg-card--content-card market-detail-card\" data-component=\"content-card\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen sg-object-card__header\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Pflichtangaben für neue Marktdurchführung</div></header><div class=\"sg-card-segment sg-card-segment--body sg-card-segment--gray\"><div class=\"sg-form-sections-card-wrapper\" data-pattern=\"form-sections\"><form class=\"sg-form-sections-card market-create-form\" data-market-create-form=\"true\"><div class=\"sg-form-sections-card__body\" data-pattern-part=\"form-body\"><div class=\"sg-form-sections-card__section\">' + fields + '</div></div><footer class=\"sg-form-sections-card__actions-segment\" data-pattern-part=\"form-actions-segment\"><div class=\"sg-form-sections-card__actions\" data-pattern-part=\"form-actions\"><button class=\"sg-interaction-element sg-button sg-button--active sg-form-sections-card__action\" type=\"button\" data-market-create-cancel>Abbrechen</button><button class=\"sg-interaction-element sg-button sg-button--process sg-form-sections-card__action\" type=\"submit\">Speichern</button></div></footer><p class=\"sg-body\" data-market-create-status aria-live=\"polite\"></p></form></div></div></article>'; };";
echo " const buildUrl = (params) => { const url = new URL(window.location.href); Object.entries(params).forEach(([key, value]) => { if (value === null || value === undefined || value === '') { url.searchParams.delete(key); } else { url.searchParams.set(key, String(value)); } }); return url; };";
echo " const replaceFragment = (html) => { const doc = new DOMParser().parseFromString(html, 'text/html'); const fragment = doc.querySelector('[data-markets-content]'); if (!fragment) { return false; } contentRoot.innerHTML = fragment.outerHTML; return Boolean(getTable()); };";
echo " const loadFragment = async (params) => { if (searchTimer) { window.clearTimeout(searchTimer); searchTimer = null; } const input = getSearchInput(); const wasFocused = input ? document.activeElement === input : false; const response = await fetch(buildUrl({ ...params, market_fragment: '1' }).toString(), { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { return; } const replaced = replaceFragment(await response.text()); if (!replaced) { return; } window.history.replaceState({}, '', buildUrl(params)); bindTable(); if (wasFocused) { const nextInput = getSearchInput(); nextInput?.focus({ preventScroll: true }); if (nextInput) { nextInput.setSelectionRange(nextInput.value.length, nextInput.value.length); } } };";
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 " const bindTable = () => { const table = getTable(); if (!table) { return; } if (window.sgInitLargeTableColumnResize) { window.sgInitLargeTableColumnResize({ root: contentRoot }); } 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'); } }); }); };";
@@ -1192,13 +1240,20 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
echo " const addMarketDayDraft = () => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; if (!payload?.occurrence) { return; } const days = Array.isArray(payload.occurrence.days) ? payload.occurrence.days : []; if (days.some((day) => day.__draft)) { return; } days.push({ id: 'draft-' + Date.now(), date: '', __draft: true }); payload.occurrence.days = days; rerenderMarketDetail(payload); const draftEdit = contentRoot.querySelector('[data-market-edit-form][data-market-edit-draft=\"true\"]')?.closest('[data-shared-editable-action-card]')?.querySelector('[data-shared-editable-card-edit]'); draftEdit?.click(); window.setTimeout(() => syncMarketDayAddButton(contentRoot), 0); };";
echo " const discardMarketDayDraft = () => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; if (!payload?.occurrence || !Array.isArray(payload.occurrence.days)) { return; } const days = payload.occurrence.days.filter((day) => !day.__draft); if (days.length === payload.occurrence.days.length) { return; } payload.occurrence.days = days; rerenderMarketDetail(payload); };";
echo " const closeMarketDrawer = () => { setMarketDrawerState(false); contentRoot.querySelectorAll('[data-market-drawer-open]').forEach((button) => button.setAttribute('aria-expanded', 'false')); };";
echo " const renderMarketDetail = (payload) => { const market = payload.market || {}; const occurrence = payload.occurrence || {}; const days = Array.isArray(occurrence.days) ? occurrence.days : []; const hasDraftDay = days.some((day) => day.__draft); const organizer = occurrence.organizer || {}; const organization = organizer.organization || {}; const contactPerson = organizer.contact_person || {}; const website = market.website_url ? '<a class=\"sg-hyperlink\" href=\"' + marketEscape(market.website_url) + '\" target=\"_blank\" rel=\"noopener\">' + marketEscape(market.website_url) + '</a>' : 'Nicht hinterlegt'; const contract = occurrence.contract_folder_url ? '<a class=\"sg-hyperlink\" href=\"' + marketEscape(occurrence.contract_folder_url) + '\" target=\"_blank\" rel=\"noopener\">Vertragsordner öffnen</a>' : 'Nicht hinterlegt'; const overview = marketRow('Markt', market.name) + marketRow('Kategorie', market.category_name || market.category_code) + '<div class=\"market-detail-row\"><dt>Website</dt><dd>' + website + '</dd></div>' + marketRow('Ort', occurrence.place) + marketRow('Adresse', occurrence.address) + marketRow('Gemeinde', occurrence.municipality_reference_id) + marketRow('Veranstalter', organization.name) + marketRow('Kontaktperson', marketContactName(contactPerson)) + marketRow('Kontakt E-Mail', contactPerson.email) + marketRow('Kontakt Telefon', contactPerson.phone); const dates = days.map((day) => marketDate(day.date)).filter((date) => date !== 'Nicht hinterlegt').join(' – '); const costSummary = occurrence.cost_summary || {}; const participation = marketRow('Anmeldung / Anfrage', occurrence.request_or_registration_date) + marketRow('Anmeldefrist', occurrence.registration_deadline) + marketRow('Bestätigung', occurrence.confirmation_date) + marketRow('Zahlungsfrist', occurrence.payment_deadline) + marketRow('Zahlungsstatus', occurrence.payment_status) + marketRow('Zahlungsdatum', occurrence.payment_date) + marketRow('Bezahlter Betrag', marketMoney(occurrence.paid_amount)) + marketRow('Zahlungsart', occurrence.payment_method_code) + marketRow('Gesamtkosten brutto', marketMoney(costSummary.gross_amount)) + marketRow('Gesamtkosten netto', marketMoney(costSummary.net_amount)) + marketRow('Steuern', marketMoney(costSummary.tax_amount)) + marketRow('Offener Betrag', marketMoney(costSummary.open_amount)) + '<div class=\"market-detail-row\"><dt>Kostenpositionen</dt><dd>' + marketDisplay(occurrence.cost_items_text) + '</dd></div>'; const infrastructure = marketRow('Standgrösse', occurrence.stand_size) + marketRow('Tisch vorhanden', marketBoolean(occurrence.table_available)) + marketRow('Tisch mietbar', marketBoolean(occurrence.table_rentable)) + marketRow('Eigener Tisch erforderlich', marketBoolean(occurrence.own_table_required)) + marketRow('Überdachung', marketBoolean(occurrence.shelter_available)) + marketRow('Strom', marketBoolean(occurrence.power_available)) + marketRow('Wasser', marketBoolean(occurrence.water_available)) + marketRow('Beleuchtung', marketBoolean(occurrence.lighting_available)) + marketRow('Weitere Infrastruktur', occurrence.other_infrastructure) + marketRow('Anforderungen an den eigenen Stand', occurrence.own_stand_requirements); const travel = marketRow('Reiseadresse', occurrence.travel_address) + marketRow('Distanz (km)', occurrence.travel_distance_km) + marketRow('Reisedauer (Minuten)', occurrence.travel_duration_minutes) + marketRow('Parkmöglichkeiten', occurrence.parking_options) + marketRow('Anlieferung', occurrence.delivery_access) + marketRow('Direktes Entladen', marketBoolean(occurrence.direct_unload_possible)) + marketRow('Lagerung', occurrence.storage_options) + marketRow('Besondere Anreisehinweise', occurrence.travel_notes); const notes = marketRow('Teilnahmebedingungen', occurrence.other_participation_conditions) + marketRow('Rücktrittsbedingungen', occurrence.cancellation_terms) + marketRow('Umsatzbeteiligung', occurrence.revenue_share_terms) + marketRow('Notizen', occurrence.notes) + '<div class=\"market-detail-row\"><dt>Vertragsordner</dt><dd>' + contract + '</dd></div>'; const salesSummary = occurrence.sales_summary || {}; const dayTabs = days.map((day, index) => { const label = day.__draft ? 'Neuer Tag' : marketDate(day.date); const selected = day.__draft || (!hasDraftDay && index === 0); return '<button class=\"sg-interaction-element sg-button sg-button--active sg-tab-button\" type=\"button\" role=\"tab\" aria-selected=\"' + (selected ? 'true' : 'false') + '\" aria-controls=\"market-day-panel-' + day.id + '\" id=\"market-day-tab-' + day.id + '\" data-market-day-tab=\"true\">' + label + '</button>'; }).join(''); const addDayTab = '<button class=\"sg-interaction-element sg-button sg-button--active sg-tab-button market-day-add-button\" type=\"button\" data-market-day-add=\"true\" aria-label=\"Markttag hinzufügen\" title=\"Markttag hinzufügen\">+</button>'; const dayPanels = days.map((day, index) => { const label = day.__draft ? 'Neuer Tag' : marketDa Line truncated
echo " const renderMarketDetailWithActions = (payload) => marketDeletePatternWithTitleAndDays(payload.occurrence?.id || '', marketDrawerTitle(payload), renderMarketDetail(payload));";
echo " const renderMarketDetail = (payload) => { const market = payload.market || {}; const occurrence = payload.occurrence || {}; const days = Array.isArray(occurrence.days) ? occurrence.days : []; const hasDraftDay = days.some((day) => day.__draft); const organizer = occurrence.organizer || {}; const organization = organizer.organization || {}; const contactPerson = organizer.contact_person || {}; const website = market.website_url ? '<a class=\"sg-hyperlink\" href=\"' + marketEscape(market.website_url) + '\" target=\"_blank\" rel=\"noopener\">' + marketEscape(market.website_url) + '</a>' : 'Nicht hinterlegt'; const contract = occurrence.contract_folder_url ? '<a class=\"sg-hyperlink\" href=\"' + marketEscape(occurrence.contract_folder_url) + '\" target=\"_blank\" rel=\"noopener\">Vertragsordner öffnen</a>' : 'Nicht hinterlegt'; const overview = marketRow('Markt', market.name) + marketRow('Kategorie', market.category_name || market.category_code) + '<div class=\"market-detail-row\"><dt>Website</dt><dd>' + website + '</dd></div>' + marketRow('Ort', occurrence.place) + marketRow('Adresse', occurrence.address) + marketRow('Gemeinde', occurrence.municipality_reference_id) + marketRow('Veranstalter', organization.name) + marketRow('Kontaktperson', marketContactName(contactPerson)) + marketRow('Kontakt E-Mail', contactPerson.email) + marketRow('Kontakt Telefon', contactPerson.phone); const dates = days.map((day) => marketDate(day.date)).filter((date) => date !== 'Nicht hinterlegt').join(' – '); const costSummary = occurrence.cost_summary || {}; const participation = marketRow('Anmeldung / Anfrage', occurrence.request_or_registration_date) + marketRow('Hinweis Anmeldung', occurrence.registration_note) + marketRow('Anmeldefrist', occurrence.registration_deadline) + marketRow('Bestätigung', occurrence.confirmation_date) + marketRow('Zahlungsfrist', occurrence.payment_deadline) + marketRow('Zahlungsstatus', occurrence.payment_status) + marketRow('Zahlungsdatum', occurrence.payment_date) + marketRow('Bezahlter Betrag', marketMoney(occurrence.paid_amount)) + marketRow('Zahlungsart', occurrence.payment_method_code) + marketRow('Gesamtkosten brutto', marketMoney(costSummary.gross_amount)) + marketRow('Gesamtkosten netto', marketMoney(costSummary.net_amount)) + marketRow('Steuern', marketMoney(costSummary.tax_amount)) + marketRow('Offener Betrag', marketMoney(costSummary.open_amount)) + '<div class=\"market-detail-row\"><dt>Kostenpositionen</dt><dd>' + marketDisplay(occurrence.cost_items_text) + '</dd></div>'; const infrastructure = marketRow('Standgrösse', occurrence.stand_size) + marketRow('Tisch vorhanden', marketBoolean(occurrence.table_available)) + marketRow('Tisch mietbar', marketBoolean(occurrence.table_rentable)) + marketRow('Eigener Tisch erforderlich', marketBoolean(occurrence.own_table_required)) + marketRow('Überdachung', marketBoolean(occurrence.shelter_available)) + marketRow('Strom', marketBoolean(occurrence.power_available)) + marketRow('Wasser', marketBoolean(occurrence.water_available)) + marketRow('Beleuchtung', marketBoolean(occurrence.lighting_available)) + marketRow('Weitere Infrastruktur', occurrence.other_infrastructure) + marketRow('Anforderungen an den eigenen Stand', occurrence.own_stand_requirements); const travel = marketRow('Reiseadresse', occurrence.travel_address) + marketRow('Distanz (km)', occurrence.travel_distance_km) + marketRow('Parkmöglichkeiten', occurrence.parking_options) + marketRow('Anlieferung', occurrence.delivery_access) + marketRow('Direktes Entladen', marketBoolean(occurrence.direct_unload_possible)) + marketRow('Lagerung', occurrence.storage_options) + marketRow('Besondere Anreisehinweise', occurrence.travel_notes); const notes = marketRow('Teilnahmebedingungen', occurrence.other_participation_conditions) + marketRow('Rücktrittsbedingungen', occurrence.cancellation_terms) + marketRow('Umsatzbeteiligung', occurrence.revenue_share_terms) + marketRow('Notizen', occurrence.notes) + '<div class=\"market-detail-row\"><dt>Vertragsordner</dt><dd>' + contract + '</dd></div>'; const salesSummary = occurrence.sales_summary || {}; const dayTabs = days.map((day, index) => { const label = day.__draft ? 'Neuer Tag' : marketDate(day.date); const selected = day.__draft || (!hasDraftDay && index === 0); return '<button class=\"sg-interaction-element sg-button sg-button--active sg-tab-button\" type=\"button\" role=\"tab\" aria-selected=\"' + (selected ? 'true' : 'false') + '\" aria-controls=\"market-day-panel-' + day.id + '\" id=\"market-day-tab-' + day.id + '\" data-market-day-tab=\"true\">' + label + '</button>'; }).join(''); const addDayTab = '<button class=\"sg-interaction-element sg-button sg-button--active sg-tab-button market-day-add-button\" type=\"button\" data-market-day-add=\"true\" aria-label=\"Markttag hinzufügen\" title=\"Markttag hinzufügen\">+</button>'; const dayPanels = days.map((day, index) => { const label = day.__draft ? 'Neuer Tag' : marketDate(day.da Line truncated
echo " const reorderMarketStatusCard = (html) => { const template = document.createElement('template'); template.innerHTML = html; const target = template.content.querySelector('[data-market-delete-occurrence-id]'); const cards = target ? Array.from(target.children).filter((child) => child.matches('.market-detail-card')) : []; if (cards.length > 1) { target.insertBefore(cards[0], cards[1].nextSibling); } return template.innerHTML; };";
echo " const renderMarketDetailWithActions = (payload) => reorderMarketStatusCard(marketDeletePatternWithTitleAndDays(payload.occurrence?.id || '', marketDrawerTitle(payload), renderMarketDetail(payload)));";
echo " const ensureChecklistTrailingItem = (form, list) => { const targetList = list || form?.querySelector('[data-checklist-edit-list]'); if (!targetList) { return; } const trailing = targetList.querySelector('[data-checklist-trailing-item]'); if (!trailing || (trailing.querySelector('[data-checklist-label]')?.value || '').trim() !== '') { const item = document.createElement('label'); item.className = 'sg-labeled-input-row market-checklist-edit-row market-checklist-edit-row--trailing'; item.dataset.checklistEditItem = ''; item.dataset.checklistTrailingItem = ''; item.dataset.checklistChapterKey = targetList.dataset.checklistChapterKey || ''; item.innerHTML = '<span class=\"sg-label\"><button class=\"sg-checkbox-field sg-checkbox-field--on-grey\" type=\"button\" role=\"checkbox\" aria-checked=\"false\" aria-label=\"Neuen Checklistenpunkt abhaken\" data-checklist-edit-checkbox><span class=\"sg-checkbox-field__mark\" aria-hidden=\"true\">✓</span></button></span><span class=\"market-checklist-edit-control\"><input class=\"sg-interaction-element sg-input-single-line\" type=\"text\" value=\"\" aria-label=\"Neuer Checklistenpunkt\" data-checklist-label></span>'; targetList.append(item); } };";
echo " const setChecklistCheckbox = (button, checked) => { if (!button) { return; } button.setAttribute('aria-checked', String(checked)); button.classList.toggle('sg-form-active', checked); };";
echo " const persistChecklistReadToggle = async (button) => { const drawer = getMarketDrawer(); const card = button.closest('[data-checklist-card]'); const status = card?.querySelector('[data-checklist-read-status]'); const index = Number(button.dataset.checklistReadCheckbox); let payload = {}; try { payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : {}; } catch (error) { return; } const sourceItems = Array.isArray(payload.occurrence?.checklist?.items) ? payload.occurrence.checklist.items : []; if (!sourceItems[index]) { return; } const previous = button.getAttribute('aria-checked') === 'true'; const next = !previous; setChecklistCheckbox(button, next); setChecklistCheckbox(card.querySelectorAll('[data-checklist-edit-checkbox]')[index], next); const items = Array.from(card.querySelectorAll('[data-checklist-read-checkbox]')).map((checkbox, itemIndex) => ({ chapter_key: sourceItems[itemIndex]?.chapter_key || '', label: sourceItems[itemIndex]?.label || '', is_completed: checkbox.getAttribute('aria-checked') === 'true' })); setChecklistSignal(card, items); button.disabled = true; if (status) { status.textContent = 'Speichern läuft …'; } 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: 'update_checklist', id: payload.occurrence.id, items: items.map((item) => ({ chapter_key: item.chapter_key, label: item.label || '', is_completed: Boolean(item.is_completed) })), comment: payload.occurrence.checklist.comment || '' }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } payload.occurrence.checklist = result.checklist; if (drawer) { drawer.dataset.marketPayload = JSON.stringify(payload); } if (status) { status.textContent = ''; } } catch (error) { setChecklistCheckbox(button, previous); setChecklistCheckbox(card.querySelectorAll('[data-checklist-edit-checkbox]')[index], previous); const rollbackItems = Array.from(card.querySelectorAll('[data-checklist-read-checkbox]')).map((checkbox, itemIndex) => ({ chapter_key: sourceItems[itemIndex]?.chapter_key || '', label: sourceItems[itemIndex]?.label || '', is_completed: checkbox.getAttribute('aria-checked') === 'true' })); setChecklistSignal(card, rollbackItems); if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'; } } finally { button.disabled = false; } };";
echo " document.addEventListener('click', (event) => { const checkbox = event.target.closest('[data-checklist-edit-checkbox]'); if (checkbox) { event.preventDefault(); setChecklistCheckbox(checkbox, checkbox.getAttribute('aria-checked') !== 'true'); const editCard = checkbox.closest('[data-checklist-card]'); setChecklistSignal(editCard, Array.from(editCard?.querySelectorAll('[data-checklist-edit-item]') || []).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' }))); return; } const readCheckbox = event.target.closest('[data-checklist-read-checkbox]'); if (readCheckbox) { event.preventDefault(); void persistChecklistReadToggle(readCheckbox); return; } const deleteItem = event.target.closest('[data-checklist-delete-item]'); if (deleteItem) { const list = deleteItem.closest('[data-checklist-edit-list]'); deleteItem.closest('[data-checklist-edit-item]')?.remove(); ensureChecklistTrailingItem(deleteItem.closest('[data-market-checklist-form]'), list); const deleteCard = deleteItem.closest('[data-checklist-card]'); setChecklistSignal(deleteCard, Array.from(deleteCard?.querySelectorAll('[data-checklist-edit-item]') || []).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' }))); } });";
echo " document.addEventListener('input', (event) => { const input = event.target.closest('[data-market-checklist-form] [data-checklist-label]'); if (!input) { return; } const form = input.closest('[data-market-checklist-form]'); const item = input.closest('[data-checklist-edit-item]'); const list = input.closest('[data-checklist-edit-list]'); if (item?.hasAttribute('data-checklist-trailing-item') && input.value.trim() !== '') { item.removeAttribute('data-checklist-trailing-item'); } ensureChecklistTrailingItem(form, list); const card = input.closest('[data-checklist-card]'); setChecklistSignal(card, Array.from(card?.querySelectorAll('[data-checklist-edit-item]') || []).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' }))); });";
echo " document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-market-checklist-form]'); if (!form) { return; } event.preventDefault(); event.stopImmediatePropagation(); const status = form.querySelector('[data-market-checklist-status]'); const checklistWasVisible = window.SharedEditableActionCard?.isVisible(form.closest('[data-checklist-card]')) === true; const items = Array.from(form.querySelectorAll('[data-checklist-edit-item]')).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ chapter_key: item.dataset.checklistChapterKey || item.closest('[data-checklist-edit-list]')?.dataset.checklistChapterKey || '', label: item.querySelector('[data-checklist-label]')?.value || '', is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' })); if (status) { status.textContent = 'Speichern läuft …'; } 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: 'update_checklist', id: form.dataset.marketChecklistId, items, comment: form.querySelector('[data-checklist-comment]')?.value || '' }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } const trigger = contentRoot.querySelector('[data-market-drawer-open][aria-expanded=\"true\"]'); if (trigger) { await openMarketDrawer(trigger); if (checklistWasVisible) { setChecklistVisibility(getMarketDrawer()?.querySelector('[data-checklist-card]'), true); } } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'; } } });";
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 });";
@@ -1211,7 +1266,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
echo " document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-market-create-form]'); if (!form) { return; } event.preventDefault(); event.stopImmediatePropagation(); const status = form.querySelector('[data-market-create-status]'); const fields = Object.fromEntries(new FormData(form).entries()); if (status) { status.textContent = 'Speichern läuft …'; } 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: 'create', fields }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } const detailResponse = await fetch('/api/market-occurrence.php?id=' + encodeURIComponent(result.occurrence_id), { credentials: 'same-origin' }); const payload = await detailResponse.json(); if (!detailResponse.ok || !payload.ok) { throw new Error(payload.error || 'Die neue Durchführung konnte nicht geladen werden.'); } const drawer = getMarketDrawer(); const title = drawer?.querySelector('[data-market-drawer-title]'); const content = drawer?.querySelector('[data-market-drawer-content]'); if (drawer) { 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 (status) { status.textContent = marketEscape(error.message); } } });";
echo " document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-market-edit-form][data-market-edit-draft=\"true\"]'); if (!form) { return; } event.preventDefault(); event.stopImmediatePropagation(); const status = form.querySelector('[data-market-edit-status]'); const fields = Object.fromEntries(new FormData(form).entries()); if (status) { status.textContent = 'Speichern läuft …'; } 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: 'create_day', id: form.dataset.marketOccurrenceId, fields }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern 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); } } });";
echo " document.addEventListener('click', (event) => { const deleteSubmit = event.target.closest('[data-market-delete-submit]'); if (deleteSubmit) { void deleteMarketOccurrence(deleteSubmit); return; } const sortButton = event.target.closest('[data-markets-sort-button]'); if (sortButton) { event.preventDefault(); void loadFragment({ market_search: getSearchValue(), market_sort: sortButton.dataset.marketsSortColumn || getSortColumn(), market_dir: sortButton.dataset.marketsSortDirection || getSortDirection(), market_limit: getPageSize() }); return; } const loadMore = event.target.closest('[data-markets-load-more-trigger]'); if (loadMore) { event.preventDefault(); void loadFragment({ market_search: getSearchValue(), market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getCurrentLimit() + getPageSize() }); return; } const trigger = event.target.closest('[data-market-drawer-open]'); if (trigger) { event.preventDefault(); if (window.matchMedia('(max-width: 767px)').matches && !marketOpeningMobileDetail) { openMarketMobileDetail(trigger); } else { void openMarketDrawer(trigger); } return; } if (event.target.closest('[data-market-drawer-close]')) { closeMarketDrawer(); return; } if (event.target.closest('[data-market-detail-back]')) { if (window.history.state?.marketView === 'detail') { window.history.back(); } else { restoreMarketList(); } return; } });";
echo " document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-market-edit-form]'); if (!form) { return; } event.preventDefault(); const status = form.querySelector('[data-market-edit-status]'); const fields = Object.fromEntries(new FormData(form).entries()); const occurrenceId = form.dataset.marketEditId || ''; if (status) { status.textContent = 'Speichern läuft …'; } 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({ scope: form.dataset.marketEditScope, id: occurrenceId, fields }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } await loadFragment({ market_search: getSearchValue(), market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getCurrentLimit() }); const refreshedTrigger = contentRoot.querySelector('[data-market-drawer-open][data-market-occurrence-id=\"' + occurrenceId + '\"]'); if (refreshedTrigger) { await openMarketDrawer(refreshedTrigger); } } catch (error) { if (status) { status.textContent = marketEscape(error.message); } } });";
echo " document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-market-edit-form]'); if (!form) { return; } event.preventDefault(); const status = form.querySelector('[data-market-edit-status]'); const fields = normalizeMarketBooleanFields(Object.fromEntries(new FormData(form).entries())); const occurrenceId = form.dataset.marketEditId || ''; if (status) { status.textContent = 'Speichern läuft …'; } 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({ scope: form.dataset.marketEditScope, id: occurrenceId, fields }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } await loadFragment({ market_search: getSearchValue(), market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getCurrentLimit() }); const refreshedTrigger = contentRoot.querySelector('[data-market-drawer-open][data-market-occurrence-id=\"' + occurrenceId + '\"]'); if (refreshedTrigger) { await openMarketDrawer(refreshedTrigger); } } catch (error) { if (status) { status.textContent = marketEscape(error.message); } } });";
echo " document.addEventListener('click', (event) => { if (event.target.closest('[data-market-drawer-backdrop]')) { closeMarketDrawer(); } });";
echo " document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { closeMarketDrawer(); restoreMarketList(); } });";
echo " window.addEventListener('popstate', () => { if (marketDetailOpen) { restoreMarketList(); } });";
@@ -1241,6 +1296,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]');
@@ -1298,14 +1355,18 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
JS;
echo " prepareMarketDefinitionDeleteCard();";
echo " const sharedCard = drawer.querySelector('[data-market-definition-card]');";
echo " const websiteInput = form.querySelector('[data-market-definition-website]'); if (websiteInput) { websiteInput.type = 'text'; }";
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 sortButton = event.target.closest('[data-market-definition-sort]'); if (!sortButton || !definitionContent.contains(sortButton)) { return; } event.preventDefault(); const url = new URL(window.location.href); url.searchParams.set('portal_module', 'Märkte'); url.searchParams.set('portal_section', 'Märkte'); url.searchParams.set('market_definition_sort', sortButton.dataset.marketDefinitionSortColumn || 'name'); url.searchParams.set('market_definition_dir', sortButton.dataset.marketDefinitionSortDirection || 'ASC'); window.location.assign(url.toString()); });";
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 +1374,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() {";
@@ -1395,12 +1458,14 @@ JS;
echo " const orderId = trigger.dataset.orderId || '';";
echo " const formatAmount = (value, currency) => value === null || value === undefined ? 'Nicht vorhanden' : currency + ' ' + Number(value).toFixed(2);";
echo " const formatDate = (value) => value ? new Date(value).toLocaleString('de-CH', { dateStyle: 'short', timeStyle: 'short' }) : 'Nicht vorhanden';";
echo " const formatMarketDay = (value) => value ? new Date(value + 'T00:00:00').toLocaleDateString('de-CH') : '';";
echo " const value = (label, content) => '<div class=\"market-detail-row\"><dt>' + escapeHtml(label) + '</dt><dd>' + escapeHtml(content || 'Nicht vorhanden') + '</dd></div>';";
echo " const renderCards = (order) => {";
echo " const c = order.customer || {}; const a = order.amounts || {}; const address = [[c.street, c.house_number].filter(Boolean).join(' '), [c.zip, c.city].filter(Boolean).join(' '), c.country_name].filter(Boolean).map(escapeHtml).join('<br>');";
echo " const lines = (order.lines || []).map((line) => '<div class=\"sg-card-segment sg-card-segment--body\"><dl class=\"market-detail-grid\">' + value(line.item_name, [line.item_code, 'Menge: ' + String(line.qty)].filter(Boolean).join(' · ')) + value('Positionsstatus', line.line_status) + value('Einzelpreis', formatAmount(line.unit_price, a.currency || 'CHF')) + value('Positionssumme', formatAmount(line.line_total, a.currency || 'CHF')) + '</dl></div>').join('');";
echo " const allocations = (order.lines || []).flatMap((line) => (line.product_allocations || []).map((item) => '<dl class=\"market-detail-grid\">' + value(item.product_name, [item.lot_number || 'Nicht zugeordnet', item.allocated_qty === null ? 'Nicht zugeordnet' : String(item.allocated_qty), item.allocation_status || 'Nicht vorhanden', item.lot_status === 'virtual_shortage' ? 'Virtuelle Fehlbestandscharge' : ''].filter(Boolean).join(' · ')) + '</dl>')).join('');";
echo " return '<article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Bestellübersicht\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Bestellübersicht</div></header><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\"><dl class=\"market-detail-grid\">' + value('Bestellnummer', order.external_ref) + value('Bestelldatum', formatDate(order.order_date)) + value('Verkaufskanal', order.order_source) + value('Bestellstatus', order.order_status) + value('Zahlungsstatus', order.payment_status) + value('Zahlungsart', order.payment_method) + value('Versandart', order.shipping_method) + value('Kunde', [c.first_name, c.last_name].filter(Boolean).join(' ')) + '<div class=\"market-detail-row\"><dt>Lieferadresse</dt><dd>' + (address || 'Nicht vorhanden') + '</dd></div>' + value('Netto', formatAmount(a.net, a.currency || 'CHF')) + value('Versand', formatAmount(a.shipping, a.currency || 'CHF')) + value('Rabatt', formatAmount(a.discount, a.currency || 'CHF')) + value('Steuer', formatAmount(a.tax, a.currency || 'CHF')) + value('Gesamtsumme', formatAmount(a.total, a.currency || 'CHF')) + '</dl></div></article><article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Bestellte Artikel\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Bestellte Artikel</div></header>' + (lines || '<div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\"><p class=\"sg-body\">Keine Artikel vorhanden.</p></div>') + '</article><article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Chargenzuordnung\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Chargenzuordnung</div></header><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\">' + (allocations || '<p class=\"sg-body\">Keine Chargenzuordnung vorhanden.</p>') + '</div></article>';";
echo " const market = order.market || {}; const marketReference = market.name && market.occurrence_day_date ? market.name + ', Marktdurchführung ' + formatMarketDay(market.occurrence_day_date) : 'kein Marktbezug';";
echo " return '<article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Bestellübersicht\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Bestellübersicht</div></header><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\"><dl class=\"market-detail-grid\">' + value('Bestellnummer', order.external_ref) + value('Bestelldatum', formatDate(order.order_date)) + value('Verkaufskanal', order.order_source) + value('Bestellstatus', order.order_status) + value('Zahlungsstatus', order.payment_status) + value('Zahlungsart', order.payment_method) + value('Versandart', order.shipping_method) + value('Kunde', [c.first_name, c.last_name].filter(Boolean).join(' ')) + '<div class=\"market-detail-row\"><dt>Lieferadresse</dt><dd>' + (address || 'Nicht vorhanden') + '</dd></div>' + value('Netto', formatAmount(a.net, a.currency || 'CHF')) + value('Versand', formatAmount(a.shipping, a.currency || 'CHF')) + value('Rabatt', formatAmount(a.discount, a.currency || 'CHF')) + value('Steuer', formatAmount(a.tax, a.currency || 'CHF')) + value('Gesamtsumme', formatAmount(a.total, a.currency || 'CHF')) + value('Markt:', marketReference) + '</dl></div></article><article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Bestellte Artikel\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Bestellte Artikel</div></header>' + (lines || '<div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\"><p class=\"sg-body\">Keine Artikel vorhanden.</p></div>') + '</article><article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Chargenzuordnung\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Chargenzuordnung</div></header><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\">' + (allocations || '<p class=\"sg-body\">Keine Chargenzuordnung vorhanden.</p>') + '</div></article>';";
echo " };";
echo " setDrawerState(true);";
echo " if (drawerTitle) { drawerTitle.textContent = trigger.dataset.orderNumber ? 'Bestellung ' + trigger.dataset.orderNumber : 'Bestellung'; }";
@@ -1627,6 +1692,9 @@ JS;
echo " closeDrawer();";
echo " }";
echo " });";
echo " document.addEventListener('otc-order-created', () => {";
echo " void reloadBestellungenSection();";
echo " });";
echo " window.addEventListener('popstate', (event) => {";
echo " if (bestellungenDetailOpen && (!event.state || event.state.bestellungenView !== 'detail')) {";
echo " restoreBestellungenList();";
@@ -1814,7 +1882,7 @@ JS;
echo " const row = document.createElement('div');";
echo " row.className = 'sg-form-sections-card__field-group';";
echo " row.dataset.otcPaymentRow = '';";
echo " row.innerHTML = '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Bezahlart</span><select class=\"sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable sg-pulldown\" id=\"otc-payment-method-' + number + '\" data-otc-payment-method>' + paymentOptions + '</select></label><label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Betrag (CHF)</span><input class=\"sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable\" type=\"number\" id=\"otc-payment-amount-' + number + '\" min=\"0.01\" step=\"0.01\" value=\"0.00\" data-otc-payment-amount></label>' ;";
echo " row.innerHTML = '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Bezahlart</span><select class=\"sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable sg-pulldown\" id=\"otc-payment-method-' + number + '\" data-otc-payment-method>' + paymentOptions + '</select></label><label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Betrag (CHF)</span><input class=\"sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable\" type=\"number\" id=\"otc-payment-amount-' + number + '\" min=\"0.01\" step=\"0.01\" placeholder=\"0.00\" data-otc-payment-amount></label>' ;";
echo " row.querySelector('[data-otc-payment-method]').value = nextPaymentMethod;";
echo " paymentRows.appendChild(row);";
echo " bindPaymentRow(row);";
@@ -1842,6 +1910,7 @@ JS;
echo " const result = await response.json();";
echo " if (response.ok && result.ok) {";
echo " submissionSucceeded = true;";
echo " document.dispatchEvent(new CustomEvent('otc-order-created', { detail: { orderId: result.orderId } }));";
echo " if (successEl) {";
echo " successEl.textContent = result.externalRef ? 'Bestellung erfolgreich erfasst! Bestellnummer: ' + result.externalRef : successDefaultText;";
echo " successEl.classList.remove('hidden');";
+88
View File
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../modules/shared/db.php';
require_once __DIR__ . '/../../modules/erp/lager/mcp.php';
final class ErpLagerMcpJsonRpcException 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');
$env = expand_env_values(parse_env_file(__DIR__ . '/../../.env'));
$request = [];
try {
erp_lager_mcp_authorize($env);
$pdo = connect_database($env);
$body = file_get_contents('php://input');
$request = json_decode($body === false ? '' : $body, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($request) || ($request['jsonrpc'] ?? '') !== '2.0') {
throw new InvalidArgumentException('Ungültige JSON-RPC-Anfrage.');
}
if (array_key_exists('params', $request) && !is_array($request['params'])) {
throw new ErpLagerMcpJsonRpcException(-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'] : [];
if ($method === 'initialize') {
$result = [
'protocolVersion' => (string) ($params['protocolVersion'] ?? '2025-03-26'),
'capabilities' => ['tools' => ['listChanged' => false]],
'serverInfo' => ['name' => 'naurua-erp-lager', 'version' => '1.0.0'],
];
} elseif ($method === 'notifications/initialized') {
http_response_code(202);
exit;
} elseif ($method === 'tools/list') {
$result = ['tools' => erp_lager_mcp_tools()];
} elseif ($method === 'tools/call') {
$tool = (string) ($params['name'] ?? '');
$arguments = is_array($params['arguments'] ?? null) ? $params['arguments'] : [];
if (array_key_exists('arguments', $params) && !is_array($params['arguments'])) {
throw new ErpLagerMcpJsonRpcException(-32602, 'Tool-Argumente müssen als Objekt übergeben werden.');
}
try {
$result = [
'content' => [[
'type' => 'text',
'text' => json_encode(erp_lager_mcp_call($pdo, $tool, $arguments), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
]],
'isError' => false,
];
} catch (InvalidArgumentException $e) {
throw new ErpLagerMcpJsonRpcException(-32602, $e->getMessage(), $e);
}
} else {
throw new ErpLagerMcpJsonRpcException(-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) {
$isAuthError = $e instanceof RuntimeException && str_contains($e->getMessage(), 'Token');
$isJsonRpcError = $e instanceof ErpLagerMcpJsonRpcException;
http_response_code($isJsonRpcError ? 200 : ($isAuthError ? 401 : 400));
echo json_encode([
'jsonrpc' => '2.0',
'id' => $request['id'] ?? null,
'error' => [
'code' => $isJsonRpcError ? $e->rpcCode() : -32600,
'message' => $e->getMessage(),
],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
+39 -10
View File
@@ -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);
}
+7
View File
@@ -63,6 +63,13 @@ if ($requestMethod === 'POST') {
$day = market_management_delete_day($pdo, $id);
echo json_encode(['ok' => true, 'day' => $day]);
exit;
} elseif (($payload['action'] ?? '') === 'update_checklist') {
$checklist = market_management_update_occurrence_checklist($pdo, $id, [
'items' => $payload['items'] ?? null,
'comment' => $payload['comment'] ?? null,
]);
echo json_encode(['ok' => true, 'checklist' => $checklist], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
} elseif ($scope === 'day') {
market_management_update_day($pdo, $id, $fields);
} elseif (in_array($scope, ['occurrence', 'status', 'overview', 'participation', 'infrastructure', 'travel', 'notes'], true)) {
+288 -155
View File
@@ -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;
@@ -62,7 +64,7 @@
--surface-menu-panel-portal: var(--color-light-grey);
--surface-help-icon: var(--color-medium-grey);
--surface-help-panel: var(--color-light-grey);
--surface-tooltip: var(--color-light-grey);
--surface-tooltip: var(--color-white);
--surface-pulldown-panel: var(--color-light-grey);
--surface-activatable-remove: var(--surface-control-default);
--surface-slider-track: var(--color-medium-grey);
@@ -77,10 +79,8 @@
--layout-pulldown-chevron-offset: var(--compact-interaction-padding-horizontal);
--layout-tab-navigation-large-padding-inline: var(--interaction-padding-horizontal);
--layout-pulldown-chevron-reserved-space: var(--spacing-large);
--layout-pulldown-panel-form-mobile-width: 90%;
--layer-pulldown-panel: var(--dimension-layer-pulldown-panel);
--layout-multiselect-pulldown-panel-desktop-width: var(--dimension-multiselect-pulldown-panel-desktop-width);
--layout-multiselect-pulldown-label-column-width-fallback: max-content;
--layout-input-label-width: var(--dimension-input-label-width);
--layout-input-field-desktop-width: var(--dimension-input-field-desktop-width);
--layout-input-field-max-width: var(--dimension-input-field-max-width);
@@ -144,7 +144,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 +309,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 +356,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 +1763,7 @@ section + section {
}
.sg-tooltip-wrap {
position: relative;
display: inline-flex;
vertical-align: middle;
display: contents;
}
.sg-tooltip-panel {
@@ -1822,33 +1818,74 @@ section + section {
}
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel::before {
top: calc(var(--spacing-small) * -1);
top: calc(var(--spacing-small) * -2);
left: 50%;
border-width: 0 var(--spacing-small) var(--spacing-small);
border-width: 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
border-color: transparent transparent var(--surface-tooltip);
transform: translateX(-50%);
}
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel::before {
bottom: calc(var(--spacing-small) * -1);
bottom: calc(var(--spacing-small) * -2);
left: 50%;
border-width: var(--spacing-small) var(--spacing-small) 0;
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
border-color: var(--surface-tooltip) transparent transparent;
transform: translateX(-50%);
}
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel::before {
top: 50%;
right: calc(var(--spacing-small) * -1);
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
right: calc(var(--spacing-small) * -2);
border-width: calc(var(--spacing-small) * 2) 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
border-color: transparent transparent transparent var(--surface-tooltip);
transform: translateY(-50%);
}
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel::before {
top: 50%;
left: calc(var(--spacing-small) * -1);
border-width: var(--spacing-small) var(--spacing-small) var(--spacing-small) 0;
left: calc(var(--spacing-small) * -2);
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
border-color: transparent var(--surface-tooltip) transparent transparent;
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) * -2);
left: var(--tooltip-arrow-offset);
border-width: 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
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) * -2);
left: var(--tooltip-arrow-offset);
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 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) * -2);
border-width: calc(var(--spacing-small) * 2) 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
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) * -2);
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
border-color: transparent var(--surface-tooltip) transparent transparent;
transform: translateY(-50%);
}
@@ -2341,6 +2378,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 +2625,44 @@ 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:not(.sg-options-row--left-only) > .sg-options-row__left:has(> .sg-pulldown-demo:nth-last-child(2):nth-child(odd)) > .sg-pulldown-demo:nth-last-child(2),
.sg-options-row:not(.sg-options-row--left-only) > .sg-options-row__left:has(> .sg-pulldown-demo:last-child:nth-child(odd)) > .sg-pulldown-demo:last-child {
flex-basis: 100%;
}
.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,
@@ -2562,15 +2670,6 @@ section + section {
width: 100%;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
width: min(
var(--layout-pulldown-panel-form-mobile-width),
calc(100vw - (2 * var(--spacing-large)))
);
min-width: 0;
max-width: calc(100vw - (2 * var(--spacing-large)));
}
.sg-options-row .sg-pulldown-panel .sg-form-sections-card-wrapper,
.sg-options-row .sg-pulldown-panel .sg-form-sections-card {
min-width: 0;
@@ -2886,39 +2985,63 @@ section + section {
padding-left: var(--layout-form-sections-interaction-indent);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > :not(.sg-form-sections-card__chapter-title) + :not(.sg-form-sections-card__chapter-title) {
margin-top: var(--spacing-large);
.sg-options-row[data-pattern="multiselektions-pulldown"] {
--layout-multiselect-panel-width: var(--layout-multiselect-pulldown-panel-desktop-width);
--layout-multiselect-panel-available-width: calc(100vw - (2 * var(--spacing-large)));
--layout-multiselect-chapter-gap: var(--spacing-large);
--layout-multiselect-column-gap: var(--spacing-large);
--layout-multiselect-control-gap: var(--spacing-small);
--layout-multiselect-visual-control-gap: calc(var(--spacing-small) * 2);
--layout-multiselect-control-row-height: var(--interaction-height);
--layout-multiselect-value-min-width: calc(var(--interaction-height) * 1.25);
--typography-multiselect-label-line-height: var(--line-height-base);
--opacity-multiselect-disabled: var(--disabled-opacity);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-form-sections-card__chapter-title + * {
margin-top: var(--spacing-large);
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > * + * {
margin-top: var(--layout-multiselect-chapter-gap);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-slider-row + .sg-slider-row,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-checkbox-field-option + .sg-checkbox-field-option,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-radio-activatable-group + .sg-radio-activatable-group {
margin-top: var(--spacing-small);
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-options-row__left > .sg-pulldown-demo {
flex: 1 1 100%;
min-width: 0;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-pulldown-panel__row + .sg-pulldown-panel__row {
margin-top: var(--spacing-large);
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-options-row__left > .sg-pulldown-demo .sg-pulldown {
width: 100%;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
--sg-multiselect-label-column-width: var(--layout-multiselect-pulldown-label-column-width-fallback);
width: min(
var(--layout-multiselect-panel-width),
var(--layout-multiselect-panel-available-width)
);
min-width: min(
var(--layout-multiselect-panel-width),
var(--layout-multiselect-panel-available-width)
);
max-width: var(--layout-multiselect-panel-available-width);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel .sg-form-sections-card-wrapper,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel .sg-form-sections-card {
min-width: 0;
width: 100%;
max-width: 100%;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] > .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
white-space: nowrap;
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] .sg-pulldown-panel__label {
white-space: normal;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
display: grid;
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr) auto;
grid-template-columns: max-content minmax(0, 1fr) auto;
align-items: center;
column-gap: var(--spacing-large);
column-gap: var(--layout-multiselect-column-gap);
row-gap: var(--layout-multiselect-control-gap);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable {
@@ -2926,37 +3049,43 @@ section + section {
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-slider {
opacity: var(--disabled-opacity);
opacity: var(--opacity-multiselect-disabled);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-slider-value,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-radio-activatable-group__choices {
opacity: var(--disabled-opacity);
opacity: var(--opacity-multiselect-disabled);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-mode-toggle {
grid-column: 1;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-label {
grid-column: 2;
grid-row: 1;
min-width: 0;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider {
grid-column: 3;
grid-column: 1 / 3;
grid-row: 2;
min-width: 0;
max-width: 100%;
width: 100%;
max-width: none;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider-value {
grid-column: 4;
grid-column: 3;
grid-row: 2;
margin: 0;
min-width: 0;
min-width: var(--layout-multiselect-value-min-width);
line-height: 1;
align-self: center;
white-space: nowrap;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
@@ -2965,22 +3094,26 @@ section + section {
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group {
display: grid;
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr);
grid-template-columns: max-content minmax(0, 1fr);
align-items: center;
column-gap: var(--spacing-large);
column-gap: var(--layout-multiselect-column-gap);
row-gap: var(--layout-multiselect-visual-control-gap);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label {
grid-column: 2;
grid-row: 1;
margin: 0;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-mode-toggle {
grid-column: 1;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-radio-activatable-group__choices {
grid-column: 3;
grid-column: 1 / -1;
grid-row: 2;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-radio-activatable-group__choices {
@@ -2989,119 +3122,45 @@ section + section {
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable > :not(.sg-mode-toggle),
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > :not(.sg-mode-toggle),
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > :not(.sg-mode-toggle) {
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__row-header > :not(.sg-mode-toggle),
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown {
pointer-events: none;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] {
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr);
column-gap: var(--spacing-large);
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: var(--layout-multiselect-control-row-height) auto;
row-gap: var(--layout-multiselect-visual-control-gap);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-mode-toggle {
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__row-header {
display: flex;
grid-column: 1;
grid-row: 1;
align-items: center;
gap: var(--layout-multiselect-column-gap);
min-width: 0;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
grid-column: 2;
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__row-header > .sg-pulldown-panel__label {
margin: 0;
min-width: 0;
line-height: var(--typography-multiselect-label-line-height);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown {
grid-column: 3;
grid-column: 1;
grid-row: 2;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled {
opacity: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__label,
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__row-header > .sg-pulldown-panel__label,
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown {
opacity: var(--disabled-opacity);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-demo .sg-pulldown-panel {
min-width: 100%;
width: max-content;
max-width: calc(100vw - (2 * var(--spacing-large)));
}
@media (max-width: 48rem) {
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
min-width: 100%;
width: max-content;
max-width: calc(100vw - (2 * var(--spacing-large)));
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
grid-template-columns: max-content minmax(0, 1fr) auto;
row-gap: var(--spacing-small);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] > .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
white-space: normal;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-mode-toggle {
grid-column: 1;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-label {
grid-column: 2;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider {
grid-column: 1 / 3;
grid-row: 2;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider-value {
grid-column: 3;
grid-row: 2;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group {
grid-template-columns: max-content minmax(0, 1fr);
row-gap: var(--spacing-small);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label {
grid-column: 2;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-mode-toggle {
grid-column: 1;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-radio-activatable-group__choices {
grid-column: 1 / -1;
grid-row: 2;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] {
grid-template-columns: max-content minmax(0, 1fr);
row-gap: var(--spacing-small);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-mode-toggle {
grid-column: 1;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
grid-column: 2;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown {
grid-column: 1 / -1;
grid-row: 2;
}
opacity: var(--opacity-multiselect-disabled);
}
.sg-form-sections-card__actions {
@@ -4308,22 +4367,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 +4407,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);
@@ -4831,16 +4900,32 @@ main {
width: 100%;
}
.sg-large-table[data-column-resize="true"] {
overflow: hidden;
}
.sg-large-table__viewport {
align-self: stretch;
flex: 1 1 auto;
min-width: 0;
width: auto;
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
box-sizing: border-box;
}
.sg-large-table__title-segment {
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: var(--spacing-large);
box-sizing: border-box;
}
.sg-large-table__row {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
grid-template-columns: var(--large-table-grid-template-columns, repeat(5, minmax(0, 1fr)));
align-items: stretch;
gap: 0;
width: 100%;
@@ -4848,6 +4933,11 @@ main {
background: var(--color-white);
}
.sg-large-table[data-column-resize="true"] .sg-large-table__row,
.sg-large-table[data-column-resize="true"] .sg-large-table__title-segment {
width: max(100%, var(--large-table-grid-min-width, 0px));
}
.sg-large-table__row--hidden {
display: none;
}
@@ -4879,16 +4969,59 @@ main {
white-space: normal;
}
.sg-large-table__load-more-layout {
.sg-large-table__row--load-more,
.sg-large-table__cell--load-more,
.sg-large-table__load-more-card {
min-width: 0;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.sg-large-table[data-column-resize="true"] .sg-large-table__row--load-more {
grid-template-columns: minmax(0, 1fr);
}
.sg-large-table__cell--header {
position: relative;
padding: 0;
}
.sg-large-table__resize-handle {
position: absolute;
z-index: 1;
top: 0;
right: -0.25rem;
bottom: 0;
width: 0.5rem;
cursor: col-resize;
touch-action: none;
}
.sg-large-table__resize-handle::after {
content: "";
position: absolute;
top: 25%;
bottom: 25%;
left: 50%;
width: 0.1rem;
transform: translateX(-50%);
background: var(--color-darkblue);
box-shadow: -0.15rem 0 0 var(--color-darkblue), 0.15rem 0 0 var(--color-darkblue);
}
.sg-large-table__resize-handle:hover::after,
.sg-large-table__resize-handle:focus-visible::after,
.sg-large-table[data-column-resize-state="resizing"] .sg-large-table__resize-handle[data-large-table-resize-active="true"]::after {
background: var(--color-darkblue);
box-shadow: -0.2rem 0 0 var(--color-darkblue), 0.2rem 0 0 var(--color-darkblue);
}
.sg-large-table__resize-handle:focus-visible {
outline: 2px solid var(--color-darkblue);
outline-offset: -1px;
}
.sg-large-table__sort-button {
display: flex;
align-items: center;
+3 -3
View File
@@ -1,7 +1,7 @@
{
"styleguideVersion": "2026.05.18.1",
"styleguideCommit": "944d791",
"syncedAtUtc": "2026-09-21T12:42:55Z",
"styleguideVersion": "2026.05.18.2",
"styleguideCommit": "08fc673",
"syncedAtUtc": "2026-10-14T11:46:02Z",
"sourceRepo": "/Users/mathias/Documents/Dokumente Chouchou/Codebases/Styleguide",
"mirroredDocsPath": "docs/styleguide"
}
+288 -155
View File
@@ -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;
@@ -62,7 +64,7 @@
--surface-menu-panel-portal: var(--color-light-grey);
--surface-help-icon: var(--color-medium-grey);
--surface-help-panel: var(--color-light-grey);
--surface-tooltip: var(--color-light-grey);
--surface-tooltip: var(--color-white);
--surface-pulldown-panel: var(--color-light-grey);
--surface-activatable-remove: var(--surface-control-default);
--surface-slider-track: var(--color-medium-grey);
@@ -77,10 +79,8 @@
--layout-pulldown-chevron-offset: var(--compact-interaction-padding-horizontal);
--layout-tab-navigation-large-padding-inline: var(--interaction-padding-horizontal);
--layout-pulldown-chevron-reserved-space: var(--spacing-large);
--layout-pulldown-panel-form-mobile-width: 90%;
--layer-pulldown-panel: var(--dimension-layer-pulldown-panel);
--layout-multiselect-pulldown-panel-desktop-width: var(--dimension-multiselect-pulldown-panel-desktop-width);
--layout-multiselect-pulldown-label-column-width-fallback: max-content;
--layout-input-label-width: var(--dimension-input-label-width);
--layout-input-field-desktop-width: var(--dimension-input-field-desktop-width);
--layout-input-field-max-width: var(--dimension-input-field-max-width);
@@ -144,7 +144,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 +309,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 +356,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 +1763,7 @@ section + section {
}
.sg-tooltip-wrap {
position: relative;
display: inline-flex;
vertical-align: middle;
display: contents;
}
.sg-tooltip-panel {
@@ -1822,33 +1818,74 @@ section + section {
}
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel::before {
top: calc(var(--spacing-small) * -1);
top: calc(var(--spacing-small) * -2);
left: 50%;
border-width: 0 var(--spacing-small) var(--spacing-small);
border-width: 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
border-color: transparent transparent var(--surface-tooltip);
transform: translateX(-50%);
}
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel::before {
bottom: calc(var(--spacing-small) * -1);
bottom: calc(var(--spacing-small) * -2);
left: 50%;
border-width: var(--spacing-small) var(--spacing-small) 0;
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
border-color: var(--surface-tooltip) transparent transparent;
transform: translateX(-50%);
}
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel::before {
top: 50%;
right: calc(var(--spacing-small) * -1);
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
right: calc(var(--spacing-small) * -2);
border-width: calc(var(--spacing-small) * 2) 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
border-color: transparent transparent transparent var(--surface-tooltip);
transform: translateY(-50%);
}
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel::before {
top: 50%;
left: calc(var(--spacing-small) * -1);
border-width: var(--spacing-small) var(--spacing-small) var(--spacing-small) 0;
left: calc(var(--spacing-small) * -2);
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
border-color: transparent var(--surface-tooltip) transparent transparent;
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) * -2);
left: var(--tooltip-arrow-offset);
border-width: 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
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) * -2);
left: var(--tooltip-arrow-offset);
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 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) * -2);
border-width: calc(var(--spacing-small) * 2) 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
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) * -2);
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
border-color: transparent var(--surface-tooltip) transparent transparent;
transform: translateY(-50%);
}
@@ -2341,6 +2378,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 +2625,44 @@ 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:not(.sg-options-row--left-only) > .sg-options-row__left:has(> .sg-pulldown-demo:nth-last-child(2):nth-child(odd)) > .sg-pulldown-demo:nth-last-child(2),
.sg-options-row:not(.sg-options-row--left-only) > .sg-options-row__left:has(> .sg-pulldown-demo:last-child:nth-child(odd)) > .sg-pulldown-demo:last-child {
flex-basis: 100%;
}
.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,
@@ -2562,15 +2670,6 @@ section + section {
width: 100%;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
width: min(
var(--layout-pulldown-panel-form-mobile-width),
calc(100vw - (2 * var(--spacing-large)))
);
min-width: 0;
max-width: calc(100vw - (2 * var(--spacing-large)));
}
.sg-options-row .sg-pulldown-panel .sg-form-sections-card-wrapper,
.sg-options-row .sg-pulldown-panel .sg-form-sections-card {
min-width: 0;
@@ -2886,39 +2985,63 @@ section + section {
padding-left: var(--layout-form-sections-interaction-indent);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > :not(.sg-form-sections-card__chapter-title) + :not(.sg-form-sections-card__chapter-title) {
margin-top: var(--spacing-large);
.sg-options-row[data-pattern="multiselektions-pulldown"] {
--layout-multiselect-panel-width: var(--layout-multiselect-pulldown-panel-desktop-width);
--layout-multiselect-panel-available-width: calc(100vw - (2 * var(--spacing-large)));
--layout-multiselect-chapter-gap: var(--spacing-large);
--layout-multiselect-column-gap: var(--spacing-large);
--layout-multiselect-control-gap: var(--spacing-small);
--layout-multiselect-visual-control-gap: calc(var(--spacing-small) * 2);
--layout-multiselect-control-row-height: var(--interaction-height);
--layout-multiselect-value-min-width: calc(var(--interaction-height) * 1.25);
--typography-multiselect-label-line-height: var(--line-height-base);
--opacity-multiselect-disabled: var(--disabled-opacity);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-form-sections-card__chapter-title + * {
margin-top: var(--spacing-large);
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > * + * {
margin-top: var(--layout-multiselect-chapter-gap);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-slider-row + .sg-slider-row,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-checkbox-field-option + .sg-checkbox-field-option,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-radio-activatable-group + .sg-radio-activatable-group {
margin-top: var(--spacing-small);
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-options-row__left > .sg-pulldown-demo {
flex: 1 1 100%;
min-width: 0;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-pulldown-panel__row + .sg-pulldown-panel__row {
margin-top: var(--spacing-large);
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-options-row__left > .sg-pulldown-demo .sg-pulldown {
width: 100%;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
--sg-multiselect-label-column-width: var(--layout-multiselect-pulldown-label-column-width-fallback);
width: min(
var(--layout-multiselect-panel-width),
var(--layout-multiselect-panel-available-width)
);
min-width: min(
var(--layout-multiselect-panel-width),
var(--layout-multiselect-panel-available-width)
);
max-width: var(--layout-multiselect-panel-available-width);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel .sg-form-sections-card-wrapper,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel .sg-form-sections-card {
min-width: 0;
width: 100%;
max-width: 100%;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] > .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
white-space: nowrap;
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] .sg-pulldown-panel__label {
white-space: normal;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
display: grid;
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr) auto;
grid-template-columns: max-content minmax(0, 1fr) auto;
align-items: center;
column-gap: var(--spacing-large);
column-gap: var(--layout-multiselect-column-gap);
row-gap: var(--layout-multiselect-control-gap);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable {
@@ -2926,37 +3049,43 @@ section + section {
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-slider {
opacity: var(--disabled-opacity);
opacity: var(--opacity-multiselect-disabled);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-slider-value,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-radio-activatable-group__choices {
opacity: var(--disabled-opacity);
opacity: var(--opacity-multiselect-disabled);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-mode-toggle {
grid-column: 1;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-label {
grid-column: 2;
grid-row: 1;
min-width: 0;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider {
grid-column: 3;
grid-column: 1 / 3;
grid-row: 2;
min-width: 0;
max-width: 100%;
width: 100%;
max-width: none;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider-value {
grid-column: 4;
grid-column: 3;
grid-row: 2;
margin: 0;
min-width: 0;
min-width: var(--layout-multiselect-value-min-width);
line-height: 1;
align-self: center;
white-space: nowrap;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
@@ -2965,22 +3094,26 @@ section + section {
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group {
display: grid;
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr);
grid-template-columns: max-content minmax(0, 1fr);
align-items: center;
column-gap: var(--spacing-large);
column-gap: var(--layout-multiselect-column-gap);
row-gap: var(--layout-multiselect-visual-control-gap);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label {
grid-column: 2;
grid-row: 1;
margin: 0;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-mode-toggle {
grid-column: 1;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-radio-activatable-group__choices {
grid-column: 3;
grid-column: 1 / -1;
grid-row: 2;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-radio-activatable-group__choices {
@@ -2989,119 +3122,45 @@ section + section {
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable > :not(.sg-mode-toggle),
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > :not(.sg-mode-toggle),
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > :not(.sg-mode-toggle) {
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__row-header > :not(.sg-mode-toggle),
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown {
pointer-events: none;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] {
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr);
column-gap: var(--spacing-large);
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: var(--layout-multiselect-control-row-height) auto;
row-gap: var(--layout-multiselect-visual-control-gap);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-mode-toggle {
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__row-header {
display: flex;
grid-column: 1;
grid-row: 1;
align-items: center;
gap: var(--layout-multiselect-column-gap);
min-width: 0;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
grid-column: 2;
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__row-header > .sg-pulldown-panel__label {
margin: 0;
min-width: 0;
line-height: var(--typography-multiselect-label-line-height);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown {
grid-column: 3;
grid-column: 1;
grid-row: 2;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled {
opacity: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__label,
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__row-header > .sg-pulldown-panel__label,
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown {
opacity: var(--disabled-opacity);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-demo .sg-pulldown-panel {
min-width: 100%;
width: max-content;
max-width: calc(100vw - (2 * var(--spacing-large)));
}
@media (max-width: 48rem) {
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
min-width: 100%;
width: max-content;
max-width: calc(100vw - (2 * var(--spacing-large)));
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
grid-template-columns: max-content minmax(0, 1fr) auto;
row-gap: var(--spacing-small);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] > .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label,
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
white-space: normal;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-mode-toggle {
grid-column: 1;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-label {
grid-column: 2;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider {
grid-column: 1 / 3;
grid-row: 2;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider-value {
grid-column: 3;
grid-row: 2;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group {
grid-template-columns: max-content minmax(0, 1fr);
row-gap: var(--spacing-small);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label {
grid-column: 2;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-mode-toggle {
grid-column: 1;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-radio-activatable-group__choices {
grid-column: 1 / -1;
grid-row: 2;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] {
grid-template-columns: max-content minmax(0, 1fr);
row-gap: var(--spacing-small);
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-mode-toggle {
grid-column: 1;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
grid-column: 2;
grid-row: 1;
}
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown {
grid-column: 1 / -1;
grid-row: 2;
}
opacity: var(--opacity-multiselect-disabled);
}
.sg-form-sections-card__actions {
@@ -4308,22 +4367,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 +4407,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);
@@ -4831,16 +4900,32 @@ main {
width: 100%;
}
.sg-large-table[data-column-resize="true"] {
overflow: hidden;
}
.sg-large-table__viewport {
align-self: stretch;
flex: 1 1 auto;
min-width: 0;
width: auto;
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
box-sizing: border-box;
}
.sg-large-table__title-segment {
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: var(--spacing-large);
box-sizing: border-box;
}
.sg-large-table__row {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
grid-template-columns: var(--large-table-grid-template-columns, repeat(5, minmax(0, 1fr)));
align-items: stretch;
gap: 0;
width: 100%;
@@ -4848,6 +4933,11 @@ main {
background: var(--color-white);
}
.sg-large-table[data-column-resize="true"] .sg-large-table__row,
.sg-large-table[data-column-resize="true"] .sg-large-table__title-segment {
width: max(100%, var(--large-table-grid-min-width, 0px));
}
.sg-large-table__row--hidden {
display: none;
}
@@ -4879,16 +4969,59 @@ main {
white-space: normal;
}
.sg-large-table__load-more-layout {
.sg-large-table__row--load-more,
.sg-large-table__cell--load-more,
.sg-large-table__load-more-card {
min-width: 0;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.sg-large-table[data-column-resize="true"] .sg-large-table__row--load-more {
grid-template-columns: minmax(0, 1fr);
}
.sg-large-table__cell--header {
position: relative;
padding: 0;
}
.sg-large-table__resize-handle {
position: absolute;
z-index: 1;
top: 0;
right: -0.25rem;
bottom: 0;
width: 0.5rem;
cursor: col-resize;
touch-action: none;
}
.sg-large-table__resize-handle::after {
content: "";
position: absolute;
top: 25%;
bottom: 25%;
left: 50%;
width: 0.1rem;
transform: translateX(-50%);
background: var(--color-darkblue);
box-shadow: -0.15rem 0 0 var(--color-darkblue), 0.15rem 0 0 var(--color-darkblue);
}
.sg-large-table__resize-handle:hover::after,
.sg-large-table__resize-handle:focus-visible::after,
.sg-large-table[data-column-resize-state="resizing"] .sg-large-table__resize-handle[data-large-table-resize-active="true"]::after {
background: var(--color-darkblue);
box-shadow: -0.2rem 0 0 var(--color-darkblue), 0.2rem 0 0 var(--color-darkblue);
}
.sg-large-table__resize-handle:focus-visible {
outline: 2px solid var(--color-darkblue);
outline-offset: -1px;
}
.sg-large-table__sort-button {
display: flex;
align-items: center;
+113 -5
View File
@@ -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);
@@ -276,7 +303,7 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
}
.sg-large-table--bestellungen .sg-large-table__row {
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1.35fr) fit-content(12.5ch);
grid-template-columns: var(--large-table-grid-template-columns, minmax(0, 1.15fr) minmax(0, 1.35fr) fit-content(12.5ch));
}
.sg-large-table--bestellungen .sg-large-table__row > :nth-child(2) {
@@ -289,7 +316,7 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
}
.sg-large-table--bestellungen .sg-large-table__row {
grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1.35fr) fit-content(12.5ch);
grid-template-columns: var(--large-table-grid-template-columns, minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1.35fr) fit-content(12.5ch));
}
.sg-large-table--bestellungen .sg-large-table__cell--amount {
@@ -312,15 +339,15 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
}
.sg-large-table--lager .sg-large-table__row {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: var(--large-table-grid-template-columns, repeat(2, minmax(0, 1fr)));
}
.sg-large-table--markets .sg-large-table__row {
grid-template-columns: repeat(6, minmax(0, 1fr));
grid-template-columns: var(--large-table-grid-template-columns, repeat(6, minmax(0, 1fr)));
}
.sg-large-table--lager[data-lager-table-key="products"] .sg-large-table__row {
grid-template-columns: 30% repeat(4, minmax(0, 1fr));
grid-template-columns: var(--large-table-grid-template-columns, 30% repeat(4, minmax(0, 1fr)));
}
.sg-large-table--lager[data-lager-table-key="products"] .sg-large-table__cell:last-child {
@@ -404,6 +431,68 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
color: var(--color-white);
}
.market-checklist-list,
.market-checklist-edit-list {
display: flex;
flex-direction: column;
gap: var(--spacing-small);
}
.market-checklist-item,
.market-checklist-edit-item {
display: flex;
align-items: center;
gap: var(--spacing-small);
}
.market-checklist-item .sg-checkbox-field,
.market-checklist-edit-row .sg-checkbox-field {
flex: 0 0 auto;
}
.market-checklist-edit-item [data-checklist-label] {
flex: 1 1 auto;
min-width: 0;
}
.market-checklist-edit-row .sg-label {
display: flex;
align-items: center;
}
.market-checklist-edit-control {
display: flex;
align-items: center;
gap: var(--spacing-small);
flex: 1 1 auto;
width: 100%;
min-width: 0;
max-width: var(--layout-form-input-field-max-width);
}
.market-checklist-edit-control [data-checklist-label] {
flex: 1 1 auto;
width: 100%;
min-width: 0;
max-width: none;
}
.market-checklist-comment-field .sg-input-multi-line {
width: 100%;
max-width: var(--layout-form-input-field-max-width);
}
.market-checklist-delete {
min-width: var(--small-interaction-element-size);
width: var(--small-interaction-element-size);
padding-inline: 0;
}
.market-checklist-comment,
.market-checklist-comment-field {
margin-top: var(--spacing-large);
}
.market-required-field-indicator {
position: relative;
top: -0.25em;
@@ -431,3 +520,22 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
[data-bestellungen-mobile-detail] .sg-navigation-card-layout {
margin-top: 0;
}
@media (max-width: 767px) {
.market-checklist-edit-row {
flex-direction: row;
align-items: center;
gap: var(--spacing-small);
}
.market-checklist-edit-row .sg-label {
min-width: 0;
flex: 0 0 auto;
margin-right: 0;
margin-bottom: 0;
}
.market-checklist-edit-control {
max-width: none;
}
}
+6 -2
View File
@@ -79,16 +79,20 @@ if ($currentUser !== null) {
'page_size' => 20,
];
$marketOccurrencesTable['has_more'] = count($marketOccurrencesTable['rows']) >= $marketLimit;
$marketDefinitionSort = (string) ($_GET['market_definition_sort'] ?? 'name');
$marketDefinitionDirection = strtoupper((string) ($_GET['market_definition_dir'] ?? 'ASC'));
$marketDefinitionsTable = [
'rows' => market_management_search_markets($pdo, [
'include_archived' => false,
'sort' => 'name',
'sort_direction' => 'ASC',
'sort' => $marketDefinitionSort,
'sort_direction' => $marketDefinitionDirection,
'limit' => 100,
'offset' => 0,
]),
'categories' => market_management_list_categories($pdo),
'organizers' => market_management_list_organizers($pdo),
'sort_column' => $marketDefinitionSort,
'sort_direction' => $marketDefinitionDirection,
];
if ((string) ($_GET['bestellungen_fragment'] ?? '') === '1') {
echo auth_render_bestellungen_content($bestellungenTable);
+245
View File
@@ -0,0 +1,245 @@
(function initLargeTableColumnResizeModule() {
const STORAGE_PREFIX = 'sg:large-table:column-widths:';
const STORAGE_VERSION = 3;
const DEFAULT_MIN_WIDTH = 128;
const KEYBOARD_STEP = 8;
const KEYBOARD_LARGE_STEP = 40;
let activeResize = null;
const getTables = (root) => root.querySelectorAll('[data-component="large-table"][data-column-resize="true"]');
const getColumnHeaders = (table) => Array.from(
table.querySelectorAll('[data-component-part="large-table-header-row"] [role="columnheader"]'),
);
const getMinWidth = (table) => {
const configured = table.dataset.columnMinWidth;
const value = configured || getComputedStyle(table).getPropertyValue('--large-table-column-min-width').trim();
if (!value) {
return DEFAULT_MIN_WIDTH;
}
const probe = document.createElement('span');
probe.style.cssText = `position:absolute;width:${value};visibility:hidden;`;
table.appendChild(probe);
const width = probe.getBoundingClientRect().width;
probe.remove();
return width > 0 ? width : DEFAULT_MIN_WIDTH;
};
const parseDefaultWidth = (rawValue, tableWidth) => {
if (!rawValue) {
return null;
}
const value = Number.parseFloat(rawValue);
if (!Number.isFinite(value) || value <= 0) {
return null;
}
if (rawValue.trim().endsWith('%')) {
return tableWidth * value / 100;
}
if (rawValue.trim().endsWith('fr')) {
return value;
}
return value;
};
const normalizeRatios = (values) => {
const safeValues = values.map((value) => (Number.isFinite(value) && value > 0 ? value : 0));
const total = safeValues.reduce((sum, value) => sum + value, 0);
if (total <= 0) {
return Array(values.length).fill(1 / values.length);
}
return safeValues.map((value) => value / total);
};
const getDefaultRatios = (table, headers, minWidth) => {
const tableWidth = Math.max(table.querySelector('[data-component-part="large-table-viewport"]')?.clientWidth || 0, minWidth * headers.length);
const configured = headers.map((header) => parseDefaultWidth(header.dataset.defaultWidth, tableWidth));
if (configured.some((width) => width === null)) {
return Array(headers.length).fill(1 / headers.length);
}
return normalizeRatios(configured);
};
const getStorageKey = (table) => `${STORAGE_PREFIX}${table.dataset.columnResizeKey || ''}`;
const readStoredRatios = (table, count) => {
const key = getStorageKey(table);
if (!table.dataset.columnResizeKey) {
return null;
}
try {
if (!window.localStorage) {
return null;
}
const stored = JSON.parse(window.localStorage.getItem(key));
if (stored?.version !== STORAGE_VERSION || !Array.isArray(stored.ratios) || stored.ratios.length !== count) {
return null;
}
if (stored.ratios.some((ratio) => !Number.isFinite(ratio) || ratio <= 0)) {
return null;
}
return normalizeRatios(stored.ratios);
} catch (error) {
return null;
}
};
const writeStoredRatios = (table, ratios) => {
if (!table.dataset.columnResizeKey) {
return;
}
try {
if (!window.localStorage) {
return;
}
window.localStorage.setItem(getStorageKey(table), JSON.stringify({
version: STORAGE_VERSION,
ratios: normalizeRatios(ratios),
}));
} catch (error) {
// Storage can be unavailable in privacy-restricted browser contexts.
}
};
const updateHandleState = (table, widths, minWidth) => {
const handles = table.querySelectorAll('[data-large-table-resize-handle]');
handles.forEach((handle) => {
const index = Number.parseInt(handle.dataset.largeTableResizeHandle, 10);
const current = widths[index];
const maximum = Math.max(minWidth, widths[index] + widths[index + 1] - minWidth);
if (table.id) {
handle.setAttribute('aria-controls', table.id);
}
handle.setAttribute('aria-valuemin', String(Math.round(minWidth)));
handle.setAttribute('aria-valuemax', String(Math.round(maximum)));
handle.setAttribute('aria-valuenow', String(Math.round(current)));
handle.setAttribute('aria-valuetext', `${Math.round(current)} Pixel`);
});
};
const getRenderedWidths = (table, count, minWidth) => {
const headers = getColumnHeaders(table);
const widths = headers.map((header) => header.getBoundingClientRect().width);
if (widths.length === count && widths.every((width) => width > 0)) {
return widths;
}
return Array(count).fill(minWidth);
};
const applyRatios = (table, ratios, minWidth) => {
const safeRatios = normalizeRatios(ratios);
const columnTemplate = safeRatios
.map((ratio) => `minmax(${Math.ceil(minWidth)}px, ${ratio}fr)`)
.join(' ');
table.style.setProperty('--large-table-grid-template-columns', columnTemplate);
table.style.setProperty('--large-table-grid-min-width', `${Math.ceil(minWidth * safeRatios.length)}px`);
table._largeTableColumnRatios = safeRatios;
table._largeTableColumnWidths = getRenderedWidths(table, safeRatios.length, minWidth);
updateHandleState(table, table._largeTableColumnWidths, minWidth);
};
const resizeColumn = (table, index, delta, persist, baseWidths = table._largeTableColumnWidths) => {
const widths = [...baseWidths];
const minWidth = getMinWidth(table);
const nextLeft = Math.max(minWidth, Math.min(widths[index] + widths[index + 1] - minWidth, widths[index] + delta));
const appliedDelta = nextLeft - widths[index];
widths[index] = nextLeft;
widths[index + 1] -= appliedDelta;
applyRatios(table, widths.map((width) => width / widths.reduce((sum, value) => sum + value, 0)), minWidth);
if (persist) {
writeStoredRatios(table, table._largeTableColumnRatios);
}
};
const initTable = (table) => {
const headers = getColumnHeaders(table);
if (headers.length < 2) {
return;
}
const minWidth = getMinWidth(table);
const stored = readStoredRatios(table, headers.length);
applyRatios(table, stored || getDefaultRatios(table, headers, minWidth), minWidth);
if (table.dataset.largeTableResizeInitialized === 'true') {
return;
}
table.dataset.largeTableResizeInitialized = 'true';
table.addEventListener('pointerdown', (event) => {
const handle = event.target.closest('[data-large-table-resize-handle]');
if (!handle || !table.contains(handle)) {
return;
}
event.preventDefault();
activeResize = {
table,
handle,
index: Number.parseInt(handle.dataset.largeTableResizeHandle, 10),
startX: event.clientX,
startWidths: [...table._largeTableColumnWidths],
};
handle.dataset.largeTableResizeActive = 'true';
table.dataset.columnResizeState = 'resizing';
handle.setPointerCapture?.(event.pointerId);
});
table.addEventListener('pointermove', (event) => {
if (!activeResize || activeResize.table !== table) {
return;
}
resizeColumn(
table,
activeResize.index,
event.clientX - activeResize.startX,
false,
activeResize.startWidths,
);
});
table.addEventListener('pointerup', () => {
if (!activeResize || activeResize.table !== table) {
return;
}
writeStoredRatios(table, table._largeTableColumnRatios);
activeResize.handle.dataset.largeTableResizeActive = 'false';
table.dataset.columnResizeState = 'idle';
activeResize = null;
});
if (typeof ResizeObserver === 'function') {
table._largeTableResizeObserver = new ResizeObserver(() => {
applyRatios(table, table._largeTableColumnRatios, getMinWidth(table));
});
table._largeTableResizeObserver.observe(table.querySelector('[data-component-part="large-table-viewport"]'));
}
table.addEventListener('keydown', (event) => {
const handle = event.target.closest('[data-large-table-resize-handle]');
if (!handle || !table.contains(handle)) {
return;
}
const index = Number.parseInt(handle.dataset.largeTableResizeHandle, 10);
const step = event.shiftKey ? KEYBOARD_LARGE_STEP : KEYBOARD_STEP;
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
event.preventDefault();
resizeColumn(table, index, event.key === 'ArrowRight' ? step : -step, true);
} else if (event.key === 'Home' || event.key === 'End') {
event.preventDefault();
const minWidth = getMinWidth(table);
const current = table._largeTableColumnWidths[index];
const target = event.key === 'Home' ? minWidth : current + table._largeTableColumnWidths[index + 1] - minWidth;
resizeColumn(table, index, target - current, true);
}
});
};
window.sgInitLargeTableColumnResize = (options = {}) => {
const root = options.root || document;
getTables(root).forEach(initTable);
};
document.addEventListener('DOMContentLoaded', () => window.sgInitLargeTableColumnResize(), { once: true });
})();
+114 -2
View File
@@ -2,9 +2,87 @@
'use strict';
const cardSelector = '[data-shared-editable-action-card]';
const scrollRestoreStates = new WeakMap();
const getCard = (element) => element.closest(cardSelector);
const setToggleEnabled = (card, enabled) => {
const toggle = card?.querySelector('[data-shared-editable-card-toggle]');
if (!toggle) {
return;
}
toggle.disabled = !enabled;
toggle.setAttribute('aria-disabled', String(!enabled));
toggle.classList.toggle('sg-button--inactive', !enabled);
};
const setVisibility = (card, visible) => {
if (!card || !card.hasAttribute('data-shared-editable-card-collapsible')) {
return;
}
const readView = card.querySelector('[data-shared-editable-card-read]');
const toggle = card.querySelector('[data-shared-editable-card-toggle]');
card.dataset.sharedEditableCardVisible = String(visible);
card.classList.toggle('shared-editable-action-card--collapsed', !visible);
if (readView && card.querySelector('[data-shared-editable-card-form]')?.hidden !== false) {
readView.hidden = !visible;
}
if (toggle) {
toggle.textContent = visible ? 'Verbergen' : 'Anzeigen';
toggle.setAttribute('aria-expanded', String(visible));
}
};
const isVisible = (card) => card?.dataset.sharedEditableCardVisible === 'true';
const getScrollContainer = (element) => {
let candidate = element?.parentElement || null;
while (candidate && candidate !== document.body) {
const style = window.getComputedStyle(candidate);
const scrollable = /(auto|scroll|overlay)/.test(style.overflowY);
if (scrollable && candidate.scrollHeight > candidate.clientHeight) {
return candidate;
}
candidate = candidate.parentElement;
}
return document.scrollingElement || document.documentElement;
};
const captureScrollPosition = (root, card) => {
const container = getScrollContainer(card);
scrollRestoreStates.set(root, {
card,
container,
top: container.scrollTop,
left: container.scrollLeft,
timer: null,
});
};
const restoreScrollPosition = (root) => {
const state = scrollRestoreStates.get(root);
if (!state) {
return;
}
if (state.timer) {
window.clearTimeout(state.timer);
}
state.timer = window.setTimeout(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (state.container?.isConnected) {
state.container.scrollTo({ top: state.top, left: state.left, behavior: 'auto' });
}
scrollRestoreStates.delete(root);
});
});
}, 100);
};
const getDirectDeleteElement = (card, selector) => Array.from(card?.children || [])
.find((child) => child.matches(selector)) || null;
@@ -17,6 +95,15 @@
const editView = card.querySelector('[data-shared-editable-card-form]');
const editButtons = card.querySelectorAll('[data-shared-editable-card-edit]');
if (card.hasAttribute('data-shared-editable-card-collapsible')) {
if (enabled) {
setVisibility(card, true);
} else if (!card.dataset.sharedEditableCardVisible) {
setVisibility(card, !card.hasAttribute('data-shared-editable-card-collapsed'));
}
setToggleEnabled(card, !enabled);
}
if (readView) {
readView.hidden = enabled;
}
@@ -28,6 +115,10 @@
editButton.setAttribute('aria-disabled', String(enabled));
editButton.classList.toggle('sg-button--inactive', enabled);
});
if (card.hasAttribute('data-shared-editable-card-collapsible') && enabled && readView) {
readView.hidden = true;
}
};
const resetDeleteConfirmation = (card) => {
@@ -91,9 +182,21 @@
root.dataset.sharedEditableActionCardsInitialized = 'true';
root.querySelectorAll(cardSelector).forEach((card) => setEditMode(card, false));
root.addEventListener('click', (event) => {
const toggle = event.target.closest('[data-shared-editable-card-toggle]');
if (toggle && root.contains(toggle)) {
const card = getCard(toggle);
if (card?.querySelector('[data-shared-editable-card-form]')?.hidden === false) {
return;
}
setVisibility(card, !isVisible(card));
return;
}
const edit = event.target.closest('[data-shared-editable-card-edit]');
if (edit && root.contains(edit)) {
setEditMode(getCard(edit), true);
const card = getCard(edit);
captureScrollPosition(root, card);
setEditMode(card, true);
return;
}
@@ -102,6 +205,7 @@
const card = getCard(cancel);
resetDeleteConfirmation(card);
setEditMode(card, false);
scrollRestoreStates.delete(root);
return;
}
@@ -117,6 +221,14 @@
}
});
const scrollObserver = new MutationObserver(() => {
const state = scrollRestoreStates.get(root);
if (state && !root.contains(state.card)) {
restoreScrollPosition(root);
}
});
scrollObserver.observe(root, { childList: true, subtree: true });
root.addEventListener('input', (event) => {
const input = event.target.closest('[data-shared-editable-card-delete-input]');
if (input && root.contains(input)) {
@@ -125,5 +237,5 @@
});
};
window.SharedEditableActionCard = { init, resetDeleteConfirmation, setEditMode };
window.SharedEditableActionCard = { init, resetDeleteConfirmation, setEditMode, setToggleEnabled, setVisibility, isVisible };
}());
+85
View File
@@ -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,24 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../modules/erp/lager/mcp.php';
function assert_test(bool $condition, string $message): void
{
if (!$condition) {
fwrite(STDERR, $message . PHP_EOL);
exit(1);
}
}
$tools = erp_lager_mcp_tools();
assert_test(count($tools) === 2, 'Expected 2 ERP lager MCP tools, got ' . count($tools));
$names = array_map(static fn (array $tool): string => $tool['name'], $tools);
assert_test($names === ['erp_lager.list_products', 'erp_lager.get_product'], 'Unexpected ERP lager MCP tool names');
$productTool = $tools[1];
assert_test($productTool['inputSchema']['required'] === ['product_id'], 'Product detail tool must require product_id');
assert_test($productTool['inputSchema']['properties']['product_id']['type'] === 'integer', 'Product ID must be an integer');
assert_test(is_object($tools[0]['inputSchema']['properties']), 'List products properties must serialize as a JSON object');
fwrite(STDOUT, "erp_lager_mcp_tools_list_test: OK\n");
@@ -0,0 +1,106 @@
<?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_array($schema['properties']) || is_object($schema['properties'])), $path . '.properties must be an object-compatible map');
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');
}
}
$properties = is_object($schema['properties'])
? get_object_vars($schema['properties'])
: $schema['properties'];
foreach ($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.get_occurrence_checklist' => 'Checkliste einer Marktdurchführung lesen',
'market_management.update_occurrence_checklist' => 'Checkliste einer Marktdurchführung vollständig ersetzen',
'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) === 23, 'Expected 23 MCP tools, got ' . count($tools));
$occurrenceTool = array_values(array_filter($tools, static fn (array $tool): bool => $tool['name'] === 'market_management.update_occurrence'))[0] ?? null;
assert_test(is_array($occurrenceTool), 'Update occurrence MCP tool must exist');
assert_test(isset($occurrenceTool['inputSchema']['properties']['registration_note']), 'MCP occurrence schema must expose registration_note');
$createOccurrenceTool = array_values(array_filter($tools, static fn (array $tool): bool => $tool['name'] === 'market_management.create_occurrence'))[0] ?? null;
assert_test(is_array($createOccurrenceTool), 'Create occurrence MCP tool must exist');
$createProperties = $createOccurrenceTool['inputSchema']['properties'];
assert_test(isset($createProperties['place'], $createProperties['days']['items']['properties']['date']), 'Create occurrence schema must expose both levels');
assert_test(!isset($createProperties['days']['items']['properties']['place']), 'Occurrence fields must not be nested under days');
$writeTools = [
'market_management.create_market', 'market_management.update_market',
'market_management.create_market_organizer', 'market_management.create_municipality',
'market_management.archive_market', 'market_management.reactivate_market',
'market_management.create_occurrence', 'market_management.update_occurrence',
'market_management.update_occurrence_checklist', 'market_management.archive_occurrence',
'market_management.reactivate_occurrence', 'market_management.create_day',
'market_management.update_day', 'market_management.delete_day',
];
foreach ($writeTools as $writeToolName) {
$writeTool = array_values(array_filter($tools, static fn (array $tool): bool => $tool['name'] === $writeToolName))[0] ?? null;
assert_test(is_array($writeTool), $writeToolName . ' must exist');
assert_test(in_array('idempotency_key', $writeTool['inputSchema']['required'] ?? [], true), $writeToolName . ' must require idempotency_key');
assert_test(isset($writeTool['inputSchema']['properties']['idempotency_key']), $writeToolName . ' must expose idempotency_key');
}
$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");
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../modules/market_management/service.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);
}
}
$plainText = "Test 'A' & <B> \"C\"";
$sanitized = market_management_sanitize_multiline_html($plainText);
assert_test($sanitized === "Test 'A' &amp; &lt;B&gt; \"C\"", 'Plain text must preserve quotes and escape markup characters once');
$link = '<a href="https://erpnaurua.imhochrain.ch/files.php?path=Marktmanagement%2Fvertrag.pdf">Vertrag</a>';
$sanitizedLink = market_management_sanitize_multiline_html($link);
assert_test(str_contains($sanitizedLink, 'class="sg-hyperlink"'), 'Allowed links must remain links');
assert_test(str_contains($sanitizedLink, 'href="https://erpnaurua.imhochrain.ch/files.php?path=Marktmanagement%2Fvertrag.pdf"'), 'Link href must remain valid');
fwrite(STDOUT, "market_management_sanitizer_test: OK\n");
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../modules/market_management/service.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);
}
}
$normalized = market_management_optional_url(['website_url' => 'www.stadtaffoltern.ch'], 'website_url');
assert_test($normalized === 'https://www.stadtaffoltern.ch', 'Website without scheme must be normalized to HTTPS');
$existingScheme = market_management_optional_url(['website_url' => 'http://www.stadtaffoltern.ch'], 'website_url');
assert_test($existingScheme === 'http://www.stadtaffoltern.ch', 'Existing HTTP scheme must be preserved');
try {
market_management_optional_url(['website_url' => 'not a url'], 'website_url');
fail_test('Invalid website URL must be rejected');
} catch (InvalidArgumentException) {
}
fwrite(STDOUT, "market_management_url_test: OK\n");