Compare commits
68
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fa9714793 | ||
|
|
d884793cb0 | ||
|
|
937d772c08 | ||
|
|
a82515673d | ||
|
|
c7ab2f2817 | ||
|
|
b8ba2db7a5 | ||
|
|
d871b94c52 | ||
|
|
4a819f7295 | ||
|
|
2ae368d2d6 | ||
|
|
9f35366449 | ||
|
|
dea0cd9e39 | ||
|
|
cc534b0ce4 | ||
|
|
84a4e6ac63 | ||
|
|
1b7e0c8a13 | ||
|
|
b0bf8753ed | ||
|
|
739ef662b9 | ||
|
|
d0aa29107d | ||
|
|
e25899b3ee | ||
|
|
01fa2bc2b6 | ||
|
|
ec6364b32b | ||
|
|
cd3bd1e1b1 | ||
|
|
332a385ccf | ||
|
|
5935144a5a | ||
|
|
1de403f39d | ||
|
|
f9b5383ae9 | ||
|
|
5dbbac35c3 | ||
|
|
414781db74 | ||
|
|
46e31c5cbd | ||
|
|
12a376c8fa | ||
|
|
46a5eb53f1 | ||
|
|
07a48bbc9c | ||
|
|
3aaa188d38 | ||
|
|
1286333ead | ||
|
|
6a77b56d34 | ||
|
|
a6cfcd4ccd | ||
|
|
80f81bd217 | ||
|
|
ccbcde70a6 | ||
|
|
f346fc68e0 | ||
|
|
7306ded7dd | ||
|
|
ae741aae61 | ||
|
|
b963b798df | ||
|
|
de2c6d0755 | ||
|
|
7f61df4052 | ||
|
|
367af9a1a7 | ||
|
|
23e1c6f9d1 | ||
|
|
345b015137 | ||
|
|
3c12bace28 | ||
|
|
817d3c10d1 | ||
|
|
900e7dee1c | ||
|
|
ca6015024b | ||
|
|
8733ce6148 | ||
|
|
85834a0498 | ||
|
|
732068ad9c | ||
|
|
ea434033f0 | ||
|
|
ebb71b4769 | ||
|
|
8c5668bf3d | ||
|
|
53baf0dfbf | ||
|
|
0c04caeb67 | ||
|
|
8b163c5734 | ||
|
|
a6007b5853 | ||
|
|
386b539f88 | ||
|
|
58c5912c30 | ||
|
|
17e07d525b | ||
|
|
dd019afff6 | ||
|
|
276a496ae8 | ||
|
|
9f10344fa7 | ||
|
|
c25d3f1b3c | ||
|
|
73e2144571 |
No files matched your search
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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 `<`, `>` oder
|
||||
`"`. Der MCP-Transport erwartet den HTML-Inhalt im JSON-String und escaped
|
||||
ihn nicht als HTML.
|
||||
|
||||
Die Route benötigt auf dem ERP-Webserver die Konfiguration
|
||||
`HERMES_DOCUMENT_ROOT=/volume1/Hermes/Hermes Naurua`. Dieser lokale Pfad ist
|
||||
@@ -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
|
||||
`<`, `>` oder `"` werden nicht verwendet. Der MCP-Transport escaped
|
||||
den HTML-Inhalt nicht.
|
||||
- Unsichere Erkennung wird nicht durch eine zusätzliche App-Freigabelogik
|
||||
behandelt; die fachliche Qualitätssicherung liegt im Hermes-Training.
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
2026.05.18.1
|
||||
2026.05.18.2
|
||||
@@ -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:<key></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>
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<li><a href="./patterns/navigation-card.html">Navigation Card</a></li>
|
||||
<li><a href="./patterns/left-navigation.html">Left Navigation</a></li>
|
||||
<li><a href="./patterns/notifications.html">Notifications</a></li>
|
||||
<li><a href="./patterns/history-graph.html">History Graph</a></li>
|
||||
<li><a href="./patterns/content-cards-group.html">Content Cards Group</a></li>
|
||||
<li><a href="./patterns/card-gruppe-mit-tastennavigation.html">Card Gruppe mit Tastennavigation</a></li>
|
||||
<li><a href="./patterns/formular-mit-abschnitten.html">Formular mit Abschnitten</a></li>
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Styleguide – Pattern History Graph</title>
|
||||
<link rel="stylesheet" href="../styleguide.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1 class="sg-main-heading">Pattern – History Graph</h1>
|
||||
|
||||
<section id="pattern-history-graph">
|
||||
<p class="sg-preview-label">Pattern: History Graph</p>
|
||||
|
||||
<div class="sg-preview-area sg-history-graph" aria-label="Verlauf von Ereignissen">
|
||||
<ol class="sg-history-graph__events">
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Aktuellstes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green" data-component-part="card-header">
|
||||
<p class="sg-body">Aktuellstes Ereignis mit einem einzelnen farbigen Segment.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Zweites Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-yellow" data-component-part="card-header">
|
||||
<p class="sg-body">Ereignis mit mehreren unabhängig gefärbten Segmenten.</p>
|
||||
</div>
|
||||
<div class="sg-card-segment sg-card-segment--body sg-card-segment--white" data-component-part="card-body">
|
||||
<p class="sg-body">Der Inhalt, zum Beispiel Datum und Beschreibung, wird individuell durch das konsumierende Portal definiert.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Ältestes dargestelltes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-red" data-component-part="card-header">
|
||||
<p class="sg-body">Ältestes dargestelltes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Viertes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green-light" data-component-part="card-header">
|
||||
<p class="sg-body">Viertes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Fünftes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-yellow" data-component-part="card-header">
|
||||
<p class="sg-body">Fünftes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Sechstes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green" data-component-part="card-header">
|
||||
<p class="sg-body">Sechstes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Siebtes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-red" data-component-part="card-header">
|
||||
<p class="sg-body">Siebtes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Achtes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green-light" data-component-part="card-header">
|
||||
<p class="sg-body">Achtes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Neuntes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-yellow" data-component-part="card-header">
|
||||
<p class="sg-body">Neuntes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Zehntes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green" data-component-part="card-header">
|
||||
<p class="sg-body">Zehntes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<!-- Wird nur gerendert, wenn weitere Ereignisse geladen werden können. -->
|
||||
<div class="sg-history-graph__navigation">
|
||||
<div class="sg-navigation-card-layout">
|
||||
<div class="sg-navigation-card-block">
|
||||
<article class="sg-card" data-component="card" data-pattern="navigation-card" aria-label="Weitere Ereignisse laden">
|
||||
<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment">
|
||||
<div class="sg-navigation-card-center">
|
||||
<a class="sg-hyperlink" href="#" data-component="hyperlink">Mehr laden</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -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">
|
||||
|
||||
@@ -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 });
|
||||
})();
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@import "./styles/03-typography-helpers.css";
|
||||
@import "./styles/10-components-interactive-elements.css";
|
||||
@import "./styles/47-patterns-file-upload.css";
|
||||
@import "./styles/48-patterns-history-graph.css";
|
||||
@import "./styles/20-patterns-portal-header.css";
|
||||
@import "./styles/21-patterns-options-row.css";
|
||||
@import "./styles/22-patterns-object-card.css";
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
--color-process-inactive: #FFAE79;
|
||||
--color-signal-green: #009101;
|
||||
--color-signal-green-light: #65A665;
|
||||
--color-signal-green-red: #C0624D;
|
||||
--color-signal-yellow: #9C7A00;
|
||||
--color-ampel-hellgelb: #D3B962;
|
||||
--color-signal-red: #9B3B2F;
|
||||
@@ -37,6 +38,7 @@
|
||||
--surface-card-header-muted: var(--color-darkbrown);
|
||||
--divider-card-segment: var(--color-white);
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-signal: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-dark);
|
||||
--text-card-transparent: var(--color-font-light);
|
||||
--layout-card-body-content-justify: flex-start;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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'],
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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) => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[character] || character));";
|
||||
echo " const marketDisplay = (value) => { if (value === null || value === undefined || value === '') { return 'Nicht hinterlegt'; } const escaped = marketEscape(value); const linked = escaped.replace(/https:\\/\\/erpnaurua\\.imhochrain\\.ch\\/files\\.php\\?path=[^\\s<]+/g, (url) => '<a class=\"sg-hyperlink\" href=\"' + url + '\" target=\"_blank\" rel=\"noopener\">' + url + '</a>'); return linked.replace(/\\r?\\n/g, '<br>'); };";
|
||||
echo " const marketDisplay = (value) => { if (value === null || value === undefined || value === '') { return 'Nicht hinterlegt'; } const escaped = marketEscape(value); const safeMarkup = escaped.replace(/<br>/g, '<br>').replace(/<strong>/g, '<strong>').replace(/<\\/strong>/g, '</strong>').replace(/<a class="sg-hyperlink" href="(https?:\\/\\/.*?)" target="_blank" rel="noopener noreferrer">/g, '<a class=\"sg-hyperlink\" href=\"$1\" target=\"_blank\" rel=\"noopener noreferrer\">').replace(/<\\/a>/g, '</a>'); return safeMarkup.replace(/\\r?\\n/g, '<br>'); };";
|
||||
echo " const marketBoolean = (value) => value === true || value === 'true' || value === 1 || value === '1' ? 'Ja' : value === false || value === 'false' || value === 0 || value === '0' ? 'Nein' : 'Nicht hinterlegt';";
|
||||
echo " const marketMoney = (value) => value === null || value === undefined || value === '' ? 'Nicht hinterlegt' : 'CHF ' + Number(value).toFixed(2);";
|
||||
echo " const marketContactName = (contact) => { if (!contact) { return 'Nicht hinterlegt'; } return marketDisplay([contact.first_name, contact.last_name].filter(Boolean).join(' ')); };";
|
||||
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');";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
})();
|
||||
@@ -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 };
|
||||
}());
|
||||
@@ -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' & <B> \"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");
|
||||
Reference in new issue
Block a user