Compare commits

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

No files matched your search

+7
View File
@@ -53,3 +53,10 @@ Diese Datei enthaelt operative Arbeitsregeln fuer Agenten in diesem Repository.
- Wenn Prozesse umgesetzt werden: verwende immer den Skill prozess-umsetzung
- Wenn UI umgesetzt werden soll: verwende immer den Skill styleguide-anwendung
- Zwingende Vorgabe für alle Arbeiten ist /docs/technical_architecture.md
## 9. MCP-Dokumentation
- Bei jeder Änderung an der MCP-Schnittstelle des Marktmanagements ist neben der
Repository-Dokumentation auch die MCP-Dokumentation unter
`/Volumes/Hermes/Hermes Naurua/Marktmanagement/MCP` zu aktualisieren.
- Die MCP-Dokumentation ist damit ein verbindlicher zusätzlicher Dokumentationsstand
für alle zukünftigen MCP-Änderungen in diesem Projekt.
+2
View File
@@ -20,6 +20,8 @@
16. `db/migrations/0020_include_stock_adjustments_in_balance.sql`
17. `db/migrations/0021_stock_lot_activation_selection.sql`
18. `db/migrations/0022_backfill_stock_lot_activation_selection.sql`
19. `db/migrations/0023_market_management.sql` bis `db/migrations/0034_market_occurrence_checklist.sql`
20. `db/migrations/0035_market_occurrence_checklist_chapters.sql`
## Beispielausfuehrung
@@ -0,0 +1,8 @@
BEGIN;
-- Reisedauer wird fachlich nicht mehr verwendet und vollständig entfernt.
ALTER TABLE market_occurrence
DROP CONSTRAINT IF EXISTS chk_market_occurrence_travel_duration,
DROP COLUMN IF EXISTS travel_duration_minutes;
COMMIT;
@@ -0,0 +1,2 @@
ALTER TABLE market_occurrence
ADD COLUMN registration_note TEXT;
@@ -0,0 +1,33 @@
BEGIN;
CREATE TABLE market_occurrence_checklist (
id BIGSERIAL PRIMARY KEY,
occurrence_id BIGINT NOT NULL UNIQUE REFERENCES market_occurrence(id) ON DELETE RESTRICT,
comment TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE market_occurrence_checklist_item (
id BIGSERIAL PRIMARY KEY,
occurrence_id BIGINT NOT NULL REFERENCES market_occurrence(id) ON DELETE RESTRICT,
position INTEGER NOT NULL,
label TEXT NOT NULL,
is_completed BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT chk_market_occurrence_checklist_item_position CHECK (position > 0)
);
CREATE INDEX idx_market_occurrence_checklist_item_occurrence
ON market_occurrence_checklist_item(occurrence_id, position, id);
INSERT INTO market_occurrence_checklist (occurrence_id)
SELECT id FROM market_occurrence;
INSERT INTO market_occurrence_checklist_item (occurrence_id, position, label)
SELECT occurrence_id, position, 'Position ' || position
FROM market_occurrence_checklist
CROSS JOIN generate_series(1, 30) AS positions(position);
COMMIT;
@@ -0,0 +1,53 @@
BEGIN;
ALTER TABLE market_occurrence_checklist_item
ADD COLUMN chapter_key TEXT;
DELETE FROM market_occurrence_checklist_item;
UPDATE market_occurrence_checklist
SET comment = NULL,
updated_at = NOW();
ALTER TABLE market_occurrence_checklist_item
ALTER COLUMN chapter_key SET NOT NULL,
ADD CONSTRAINT chk_market_occurrence_checklist_item_chapter
CHECK (chapter_key IN ('products_sales', 'decoration', 'stand_equipment', 'technology'));
INSERT INTO market_occurrence_checklist_item
(occurrence_id, chapter_key, position, label, is_completed)
SELECT checklist.occurrence_id,
defaults.chapter_key,
defaults.position,
defaults.label,
FALSE
FROM market_occurrence_checklist checklist
CROSS JOIN (
VALUES
('products_sales', 1, 'Fläschchen'),
('products_sales', 2, 'Säckchen'),
('products_sales', 3, 'Dunkle Kleber'),
('products_sales', 4, 'Naurua-Kleber'),
('products_sales', 5, 'Anwendungsbeilage'),
('products_sales', 6, 'Flyer Naurua'),
('products_sales', 7, 'Flyer Vitalpilze'),
('products_sales', 8, 'Vitalpilzbuch'),
('products_sales', 9, 'SumUp-Terminal'),
('products_sales', 10, 'Bargeld'),
('decoration', 1, 'Dekoration'),
('decoration', 2, 'Pilze'),
('decoration', 3, 'Blumenvase'),
('decoration', 4, 'Blume'),
('stand_equipment', 1, 'Tisch'),
('stand_equipment', 2, 'Tischtücher'),
('stand_equipment', 3, 'Beleuchtung'),
('stand_equipment', 4, 'Holzklötze'),
('stand_equipment', 5, 'Wagen'),
('stand_equipment', 6, 'Sonnenschirm'),
('stand_equipment', 7, 'Parkkarte/-Marktinformationen'),
('technology', 1, 'Batterie-/Powerbank fürs Handy')
) AS defaults(chapter_key, position, label);
CREATE INDEX idx_market_occurrence_checklist_item_chapter
ON market_occurrence_checklist_item(occurrence_id, chapter_key, position, id);
COMMIT;
+3
View File
@@ -42,6 +42,9 @@ Bestand, Charge, MHD und Bewegungen.
## Technisch umgesetzt
- `modules/erp/lager/service.php`
- Read-only MCP für Hermes: `modules/erp/lager/mcp.php`
- MCP-Endpoint: `public/api/erp-lager-mcp.php`
- [ERP-Lager-Hermes-MCP-API](konzepte/erp_lager_hermes_mcp_api.md)
Hersteller-Stammdaten gehören zum Modul `kontakte`. Eine reale Lagercharge kann dort einen Hersteller und dessen Hersteller-Chargennummer referenzieren; die interne Naurua-Chargennummer bleibt Eigentum des Moduls `lager`. Virtuelle Fehlbestandschargen haben keinen Herstellerbezug.
@@ -0,0 +1,31 @@
# Hermes: ERP-Lager-MCP-API
## Zweck
Das ERP-Lager stellt ein eigenes, read-only MCP-Interface bereit. Hermes kann dadurch ausschließlich die Lager-Tools laden, ohne Marktmanagement-Tools in seinen Kontext aufzunehmen.
Endpoint: `https://erpnaurua.imhochrain.ch/api/erp-lager-mcp.php`
Authentifizierung: `Authorization: Bearer <MARKET_MCP_TOKEN>`
Der Token ist identisch mit dem Marktmanagement-MCP, weil derselbe Hermes-Bot auf beide fachlich getrennten Interfaces zugreift. Die Toolsets und Endpoints bleiben trotzdem getrennt.
## Verfügbare Tools
### `erp_lager.list_products`
Liefert alle aktiven Produkte mit ihren stabilen IDs.
### `erp_lager.get_product`
Liest alle Lagerdaten eines aktiven Produkts. Eingabe: `{ "product_id": 12 }`.
Die Antwort enthält die GUI-parallele Produktübersicht mit Produktstammdaten, aktueller und verfügbarer Lagermenge, Menge im Wareneingang, aktueller Charge, Ausverkaufsdatum und Forecast-Status, nächster aktiver offener Charge, weiteren offenen zukünftigen Chargen, historischen geschlossenen Chargen sowie virtueller Fehlbestandsmenge und virtueller Fehlbestandscharge.
Das Interface ist ausschließlich lesend. Es gibt keine Lager-MCP-Tools zum Anlegen, Ändern, Buchen, Verschieben oder Löschen.
## JSON-RPC
Das Interface unterstützt `initialize`, `notifications/initialized`, `tools/list` und `tools/call` gemäß dem bestehenden Marktmanagement-MCP-Transport.
Bei `tools/list` ist `inputSchema.properties` für jedes Werkzeug immer ein JSON-Objekt; beim parameterlosen `erp_lager.list_products` ist es `{}`.
@@ -13,6 +13,8 @@ Status: Verbindliches DB-Ownership-Konzept
| `market_occurrence_organizer` | `market_management` | Marktmanagement-Service über Kontakt-Schnittstelle |
| `market_occurrence_day` | `market_management` | Marktmanagement-Service |
| `market_occurrence_cost_item` | `market_management` | Marktmanagement-Service |
| `market_occurrence_checklist` | `market_management` | Submodul `occurrence_checklist` |
| `market_occurrence_checklist_item` | `market_management` | Submodul `occurrence_checklist` |
| `market_municipality_reference` | `market_management` | Referenzdatenprozess |
| `market_municipality` | `market_management` | Marktmanagement-Service |
| `market_mcp_idempotency` | `market_management` | MCP-Submodul |
@@ -1,8 +1,11 @@
# Hermes: Market-Management-MCP-API
Stand: 2026-09-20
Stand: 2026-09-27
Status: Aktive technische Schnittstellendokumentation
Die Marktdurchführungs-Schnittstelle enthält kein Feld `travel_duration_minutes`.
Die Reisedauer wird nicht gespeichert, gelesen, geändert oder ausgegeben.
## Verbindung
Hermes verwendet den MCP-Endpunkt der DEV-App:
@@ -135,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`
@@ -170,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`
@@ -304,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,6 +68,7 @@ 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 interner `<a>`-Link im
@@ -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.
+20 -16
View File
@@ -77,8 +77,10 @@
<p class="sg-preview-label">Component: Large Table</p>
<p>Suche, Sortierung und „Mehr laden“ sind hier Demo-Markup. Produktions-Implementierungen müssen Treffer serverseitig neu rendern.</p>
<p>Optionale Spaltenbreiten-Anpassung: <code>data-column-resize="true"</code> aktiviert die Funktion. Die Tabelle benötigt dafür eine stabile, portalweit eindeutige <code>data-column-resize-key</code>. Die Header-Zellen können mit <code>data-default-width</code> (Prozent, Pixel oder relative <code>fr</code>-Werte) und die Tabelle mit <code>data-column-min-width</code> konfiguriert werden. Die Runtime wird nach dynamischem Nachladen über <code>window.sgInitLargeTableColumnResize({ root })</code> erneut aufgerufen. Gespeicherte Werte liegen als <code>{ version: 3, ratios: number[] }</code> unter <code>sg:large-table:column-widths:&lt;key&gt;</code> in <code>localStorage</code>; die Werte sind proportionale Anteile der verfügbaren Tabellenbreite.</p>
<article class="sg-card sg-large-table" data-component="large-table" role="table" aria-label="Beispiel Large Table">
<article id="styleguide-large-table" class="sg-card sg-large-table" data-component="large-table" data-column-resize="true" data-column-resize-key="styleguide-large-table" data-column-min-width="8rem" role="table" aria-label="Beispiel Large Table">
<div class="sg-large-table__viewport" data-component-part="large-table-viewport">
<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header">
<div class="sg-strong">Large Table</div>
<span class="sg-search-field-row" data-component="search-field">
@@ -96,31 +98,35 @@
</div>
<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--header" role="row" data-component-part="large-table-header-row">
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="0">
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="0" data-default-width="20%">
<button class="sg-large-table__sort-button" type="button" aria-label="Spalte 1 sortieren">
<span class="sg-large-table__sort-label">Spalte 1</span>
<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="ascending"></span>
</button>
<span class="sg-large-table__resize-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Breite von Spalte 1 anpassen" data-large-table-resize-handle="0"></span>
</div>
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="1">
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="1" data-default-width="20%">
<button class="sg-large-table__sort-button" type="button" aria-label="Spalte 2 sortieren">
<span class="sg-large-table__sort-label">Spalte 2</span>
<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="ascending"></span>
</button>
<span class="sg-large-table__resize-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Breite von Spalte 2 anpassen" data-large-table-resize-handle="1"></span>
</div>
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="2">
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="2" data-default-width="20%">
<button class="sg-large-table__sort-button" type="button" aria-label="Spalte 3 sortieren">
<span class="sg-large-table__sort-label">Spalte 3</span>
<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="ascending"></span>
</button>
<span class="sg-large-table__resize-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Breite von Spalte 3 anpassen" data-large-table-resize-handle="2"></span>
</div>
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="3">
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="3" data-default-width="20%">
<button class="sg-large-table__sort-button" type="button" aria-label="Spalte 4 sortieren">
<span class="sg-large-table__sort-label">Spalte 4</span>
<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="ascending"></span>
</button>
<span class="sg-large-table__resize-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Breite von Spalte 4 anpassen" data-large-table-resize-handle="3"></span>
</div>
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="4">
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="4" data-default-width="20%">
<button class="sg-large-table__sort-button" type="button" aria-label="Spalte 5 sortieren">
<span class="sg-large-table__sort-label">Spalte 5</span>
<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="ascending"></span>
@@ -210,22 +216,20 @@
<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--load-more" role="row" data-component-part="large-table-row" data-large-table-load-more-row="true">
<div class="sg-large-table__cell sg-large-table__cell--load-more" role="cell">
<div class="sg-navigation-card-layout sg-large-table__load-more-layout">
<div class="sg-navigation-card-block">
<article class="sg-card" data-component="card" data-pattern="navigation-card" aria-label="Navigations-Card">
<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment">
<div class="sg-navigation-card-center">
<a class="sg-hyperlink" href="#" data-component="hyperlink" data-large-table-load-more-trigger="true">Mehr laden</a>
</div>
</div>
</article>
<article class="sg-card sg-large-table__load-more-card" data-component="card" data-pattern="navigation-card" aria-label="Navigations-Card">
<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment">
<div class="sg-navigation-card-center">
<a class="sg-hyperlink" href="#" data-component="hyperlink" data-large-table-load-more-trigger="true">Mehr laden</a>
</div>
</div>
</div>
</article>
</div>
</div>
</div>
</article>
</section>
<script src="../scripts/large-table-column-resize.js"></script>
</body>
</html>
+19 -1
View File
@@ -7,6 +7,7 @@ Der Styleguide bleibt in diesem Repository die Source of Truth. Der Sync verteil
- den deploy-relevanten CSS-Upstream versioniert als `public/assets/styleguide.upstream.css`
- die vollstaendige Styleguide-Dokumentation gespiegelt nach `docs/styleguide`
- die portal-spezifische Fertig-CSS-Datei als `public/assets/styles.css`
- die konsumierbare Large-Table-Runtime als `public/scripts/large-table-column-resize.js`
Zielrepos:
@@ -31,8 +32,10 @@ Beispiel:
- `public/assets/styleguide.upstream.css` aktualisiert
- `public/assets/styleguide.upstream.meta.json` aktualisiert (Version, Commit, Zeitstempel)
- `docs/styleguide` gespiegelt (mit `--delete`, ohne `.git`, `.codex`, `AGENTS.md`, `scripts/`)
- `docs/styleguide` gespiegelt (mit `--delete`, ohne `.git`, `.codex`, `AGENTS.md` und interne `scripts/`)
- `public/scripts/large-table-column-resize.js` aus der Styleguide-Source-of-Truth kopiert
- `public/assets/styles.css` aktualisiert
- `public/scripts/large-table-column-resize.js` aktualisiert; lokale Portal-Kopien sind nicht zulässig
- Optional: automatischer Commit + Push im Portalrepo
## Standardprozess je Release
@@ -42,3 +45,18 @@ Beispiel:
3. Styleguide commit + push
4. Sync-Skript ausfuehren
5. Beide Portalrepos Smoke-Testen
## Large-Table-Runtime
Die Runtime liegt ausschließlich im Styleguide unter `scripts/large-table-column-resize.js`.
Der Sync kopiert nur diese explizit freigegebene Runtime nach `public/scripts/`; andere
Styleguide-Scripts wie Prüf- und Sync-Shellscripts werden nicht in `docs/styleguide/scripts/`
gespiegelt. Dadurch bleibt der Styleguide die Source of Truth und ein erneuter Sync
überschreibt die Portaldatei kontrolliert.
Für den Smoke-Test in beiden Portalen ist zu prüfen:
- `public/scripts/large-table-column-resize.js` ist vorhanden und unter `/scripts/large-table-column-resize.js` ladbar.
- Die Large-Table-Seite lädt die Runtime ohne lokale Inline- oder Duplikatimplementierung.
- Nach einer Spaltenbreitenänderung wird der Eintrag unter `sg:large-table:column-widths:<key>` gespeichert.
- Ein erneuter Sync ersetzt die Datei aus der Styleguide-Quelle.
+1 -2
View File
@@ -192,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>
@@ -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`;
});
});
+1 -1
View File
@@ -17,7 +17,7 @@
</p>
<p class="sg-table-label" style="color: var(--text-options-row-description);">
Mobile: Pulldowns stehen mit maximal zwei Elementen pro Zeile; ein Suchfeld steht immer allein in einer 100-%-Zeile.
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">
@@ -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 });
})();
@@ -199,7 +199,6 @@
<tr><td>layout-pulldown-padding-inline</td><td>compact-interaction-padding-horizontal</td><td>Innenabstand links für Pulldown-Trigger und Select-Felder.</td></tr>
<tr><td>layout-pulldown-chevron-offset</td><td>compact-interaction-padding-horizontal</td><td>Rechter Innenabstand des Chevron-Icons im Pulldown.</td></tr>
<tr><td>layout-pulldown-chevron-reserved-space</td><td>spacing-large</td><td>Reservierte Breite zwischen Pulldown-Text und rechtem Chevron-Bereich.</td></tr>
<tr><td>layout-pulldown-panel-form-mobile-width</td><td>90%</td><td>Zielbreite für Pulldown-Panels mit Formularinhalt auf Mobile; wird durch die Viewport-Kappe begrenzt.</td></tr>
<tr><td>layer-pulldown-panel</td><td>dimension-layer-pulldown-panel</td><td>Layer-Stufe geöffneter Pulldown-Ausklappfelder über übrigen Seiteninhalten.</td></tr>
<tr><td>layout-multiselect-pulldown-panel-desktop-width</td><td>dimension-multiselect-pulldown-panel-desktop-width</td><td>Semantische Obergrenze für die Panelbreite geöffneter Pulldowns; das Panel darf über Triggerbreite wachsen, bleibt aber durch diese Breite und die Viewport-Kappe begrenzt.</td></tr>
<tr><td>surface-menu-panel-portal</td><td>color-light-grey</td><td>Fläche des Sandwich-Menü-Panels für große und kleine Variante.</td></tr>
@@ -224,7 +223,7 @@
<tr><td>text-help-icon</td><td>color-font-dark</td><td>Textfarbe im Help-Icon.</td></tr>
<tr><td>text-help-panel</td><td>color-font-dark</td><td>Textfarbe im Help-Panel.</td></tr>
<tr><td>layout-help-panel-width</td><td>calc(interaction-height * 8)</td><td>Standardbreite des Help-Panels.</td></tr>
<tr><td>surface-tooltip</td><td>color-light-grey</td><td>Fläche der generischen Tooltip-Sprechblase.</td></tr>
<tr><td>surface-tooltip</td><td>color-white</td><td>Fläche der generischen Tooltip-Sprechblase.</td></tr>
<tr><td>text-tooltip</td><td>color-font-dark</td><td>Textfarbe der generischen Tooltip-Sprechblase.</td></tr>
<tr><td>layout-tooltip-width</td><td>calc(interaction-height * 8)</td><td>Standardbreite der generischen Tooltip-Sprechblase.</td></tr>
<tr><td>radius-tooltip</td><td>radius-interaction</td><td>Radius der generischen Tooltip-Sprechblase.</td></tr>
+10 -1
View File
@@ -129,7 +129,16 @@
<h3 class="sg-sub-heading sg-section-h3">Multiselektions-Pulldown</h3>
<table class="sg-foundation-table sg-table-label"><thead><tr><th>Semantischer Token</th><th>Verwendeter Foundation-Token</th><th>Beschreibung Verwendungszweck im Pattern</th></tr></thead><tbody>
<tr><td>layout-multiselect-pulldown-label-column-width-fallback</td><td>max-content</td><td>Fallback-Breite der gemeinsamen Labelspalte im Panel, bis die längste Labelbreite per Pattern-Logik ermittelt wurde.</td></tr>
<tr><td>layout-multiselect-panel-width</td><td>layout-multiselect-pulldown-panel-desktop-width</td><td>Feste Breite des Multiselektions-Pulldown-Panels auf allen Viewports innerhalb der vorhandenen Viewport-Kappe.</td></tr>
<tr><td>layout-multiselect-panel-available-width</td><td>Viewportbreite minus 2 × spacing-large</td><td>Viewport-Kappe für das Multiselektions-Pulldown-Panel mit gleichmäßigem Außenabstand.</td></tr>
<tr><td>layout-multiselect-chapter-gap</td><td>spacing-large</td><td>Einheitlicher vertikaler Abstand zwischen allen direkten Elementen eines Multiselect-Blocks.</td></tr>
<tr><td>layout-multiselect-column-gap</td><td>spacing-large</td><td>Horizontaler Abstand zwischen Aktivierungsschalter und Label in den Pattern-Zeilen.</td></tr>
<tr><td>layout-multiselect-control-gap</td><td>spacing-small</td><td>Standardabstand innerhalb der Slider-Zeilen.</td></tr>
<tr><td>layout-multiselect-visual-control-gap</td><td>2 × spacing-small</td><td>Visueller Abstand zwischen Kopfzeile und Interaktionselement bei Radio- und Pulldown-Zeilen.</td></tr>
<tr><td>layout-multiselect-control-row-height</td><td>interaction-height</td><td>Höhe der Kopfzeile in den filterbaren Pulldown-Zeilen.</td></tr>
<tr><td>layout-multiselect-value-min-width</td><td>1.25 × interaction-height</td><td>Mindestbreite der Slider-Wertanzeige.</td></tr>
<tr><td>typography-multiselect-label-line-height</td><td>line-height-base</td><td>Zeilenhöhe der Labels in den filterbaren Pulldown-Zeilen.</td></tr>
<tr><td>opacity-multiselect-disabled</td><td>disabled-opacity</td><td>Deckkraft deaktivierter Multiselect-Inhalte.</td></tr>
</tbody></table>
<h3 class="sg-sub-heading sg-section-h3">Overlay Card</h3>
+1 -3
View File
@@ -64,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);
@@ -79,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);
@@ -1295,33 +1295,33 @@
}
.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%);
}
@@ -1336,33 +1336,33 @@
}
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="bottom"]::before {
top: calc(var(--spacing-small) * -1);
top: calc(var(--spacing-small) * -2);
left: var(--tooltip-arrow-offset);
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-panel[data-tooltip-portal="true"][data-tooltip-placement="top"]::before {
bottom: calc(var(--spacing-small) * -1);
bottom: calc(var(--spacing-small) * -2);
left: var(--tooltip-arrow-offset);
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-panel[data-tooltip-portal="true"][data-tooltip-placement="left"]::before {
top: var(--tooltip-arrow-offset);
right: calc(var(--spacing-small) * -1);
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
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) * -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%);
}
@@ -105,6 +105,11 @@
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%;
}
@@ -131,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 {
@@ -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;
+10
View File
@@ -394,6 +394,8 @@ function get_sales_order_detail(PDO $pdo, int $orderId): ?array
so.amount_discount,
so.total_amount,
so.currency,
md.name AS market_name,
mday.date AS market_occurrence_day_date,
COALESCE(ad.first_name, '') AS first_name,
COALESCE(ad.last_name, '') AS last_name,
COALESCE(ad.street, '') AS street,
@@ -413,6 +415,10 @@ function get_sales_order_detail(PDO $pdo, int $orderId): ?array
) ad ON TRUE
LEFT JOIN payment_method pm ON pm.id = so.payment_method_id
LEFT JOIN shipping_method sm ON sm.id = so.shipping_method_id
LEFT JOIN market_occurrence mo ON mo.id = so.market_occurrence_id
LEFT JOIN market_definition md ON md.id = mo.market_definition_id
LEFT JOIN market_occurrence_day mday
ON mday.id = so.market_occurrence_day_id AND mday.occurrence_id = mo.id
WHERE so.id = :order_id"
);
$orderStmt->execute([':order_id' => $orderId]);
@@ -499,6 +505,10 @@ function get_sales_order_detail(PDO $pdo, int $orderId): ?array
'payment_status' => (string) $order['payment_status'],
'payment_method' => (string) $order['payment_method'],
'shipping_method' => (string) $order['shipping_method'],
'market' => [
'name' => $order['market_name'] !== null ? (string) $order['market_name'] : null,
'occurrence_day_date' => $order['market_occurrence_day_date'] !== null ? (string) $order['market_occurrence_day_date'] : null,
],
'customer' => [
'first_name' => (string) $order['first_name'],
'last_name' => (string) $order['last_name'],
+91
View File
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/service.php';
function erp_lager_mcp_token(array $env): string
{
$token = env_value('MARKET_MCP_TOKEN', $env);
if ($token === '') {
throw new RuntimeException('MARKET_MCP_TOKEN ist nicht konfiguriert.');
}
return $token;
}
function erp_lager_mcp_authorize(array $env): string
{
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!preg_match('/^Bearer\s+(.+)$/i', $header, $matches)) {
throw new RuntimeException('MCP Authorization Bearer Token fehlt.');
}
$token = trim($matches[1]);
if (!hash_equals(erp_lager_mcp_token($env), $token)) {
throw new RuntimeException('MCP Authorization Bearer Token ist ungültig.');
}
return 'hermes';
}
function erp_lager_mcp_tools(): array
{
return [
[
'name' => 'erp_lager.list_products',
'description' => 'Aktive ERP-Lagerprodukte zur Ermittlung der Produkt-IDs lesen',
'inputSchema' => ['type' => 'object', 'properties' => (object) []],
],
[
'name' => 'erp_lager.get_product',
'description' => 'Produkt, aktuelle Lagermenge, Chargen und Ausverkaufsprognose lesen',
'inputSchema' => [
'type' => 'object',
'required' => ['product_id'],
'properties' => [
'product_id' => ['type' => 'integer'],
],
],
],
];
}
function erp_lager_mcp_list_products(PDO $pdo): array
{
return array_map(
static fn (array $product): array => [
'id' => $product['id'],
'name' => $product['name'],
'sku' => $product['sku'],
'uom' => $product['uom'],
],
get_otc_order_form_products($pdo)
);
}
function erp_lager_mcp_get_product(PDO $pdo, int $productId): array
{
if ($productId < 1) {
throw new InvalidArgumentException('product_id muss eine positive Ganzzahl sein.');
}
foreach (get_lager_product_overview($pdo) as $product) {
if ((int) $product['id'] === $productId) {
return $product;
}
}
throw new InvalidArgumentException('Aktives Lagerprodukt nicht gefunden.');
}
function erp_lager_mcp_call(PDO $pdo, string $tool, array $arguments): array
{
return match ($tool) {
'erp_lager.list_products' => erp_lager_mcp_list_products($pdo),
'erp_lager.get_product' => erp_lager_mcp_get_product(
$pdo,
(int) ($arguments['product_id'] ?? 0)
),
default => throw new InvalidArgumentException('Unbekanntes Lager-MCP-Tool: ' . $tool),
};
}
+29 -14
View File
@@ -30,11 +30,13 @@ function market_management_mcp_authorize(array $env): string
function market_management_mcp_tools(): array
{
$idempotencyProperty = ['idempotency_key' => ['type' => 'string', 'minLength' => 1]];
$occurrenceProperties = [
'status' => ['type' => 'string'], 'place' => ['type' => ['string', 'null']], 'address' => ['type' => ['string', 'null']],
'municipality_reference_id' => ['type' => ['integer', 'null']], 'notes' => ['type' => ['string', 'null']],
'registration_deadline' => ['type' => ['string', 'null'], 'format' => 'date'],
'request_or_registration_date' => ['type' => ['string', 'null'], 'format' => 'date'],
'registration_note' => ['type' => ['string', 'null']],
'confirmation_date' => ['type' => ['string', 'null'], 'format' => 'date'],
'payment_deadline' => ['type' => ['string', 'null'], 'format' => 'date'],
'payment_status' => ['type' => ['string', 'null'], 'enum' => ['paid', 'unpaid']],
@@ -46,7 +48,7 @@ function market_management_mcp_tools(): array
'power_available' => ['type' => ['boolean', 'null']], 'water_available' => ['type' => ['boolean', 'null']],
'lighting_available' => ['type' => ['boolean', 'null']], 'other_infrastructure' => ['type' => ['string', 'null']],
'own_stand_requirements' => ['type' => ['string', 'null']], 'travel_address' => ['type' => ['string', 'null']],
'travel_distance_km' => ['type' => ['number', 'null']], 'travel_duration_minutes' => ['type' => ['integer', 'null']],
'travel_distance_km' => ['type' => ['number', 'null']],
'parking_options' => ['type' => ['string', 'null']], 'delivery_access' => ['type' => ['string', 'null']],
'direct_unload_possible' => ['type' => ['boolean', 'null']], 'storage_options' => ['type' => ['string', 'null']],
'travel_notes' => ['type' => ['string', 'null']], 'cancellation_terms' => ['type' => ['string', 'null']],
@@ -74,24 +76,26 @@ function market_management_mcp_tools(): array
['name' => 'market_management.search_markets', 'description' => 'Märkte suchen', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]],
['name' => 'market_management.search_occurrences', 'description' => 'Marktdurchführungen suchen und auflisten', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]],
['name' => 'market_management.get_market', 'description' => 'Markt mit Marktdurchführungen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]],
['name' => 'market_management.create_market', 'description' => 'Markt mit verpflichtendem Veranstalter, Kategorie und optionaler Website-URL anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'category_id', 'organizer_id'], 'properties' => ['name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]],
['name' => 'market_management.update_market', 'description' => 'Markt inklusive Veranstalter und Website-URL ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer'], 'name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]],
['name' => 'market_management.archive_market', 'description' => 'Markt archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer']]]],
['name' => 'market_management.reactivate_market', 'description' => 'Markt reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer']]]],
['name' => 'market_management.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'], 'properties' => ['name' => ['type' => 'string'], 'email' => ['type' => ['string', 'null'], 'format' => 'email'], 'phone' => ['type' => ['string', 'null']]]]],
['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'], 'properties' => ['name' => ['type' => 'string']]]],
['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);
}
+59 -9
View File
@@ -2,6 +2,7 @@
declare(strict_types=1);
require_once __DIR__ . '/../shared/db.php';
require_once __DIR__ . '/occurrence_checklist.php';
const MARKET_MANAGEMENT_STATUSES = [
'open',
@@ -19,7 +20,7 @@ const MARKET_OCCURRENCE_DAY_INHERITED_FIELDS = [
];
const MARKET_MANAGEMENT_MULTILINE_FIELDS = [
'address', 'notes', 'other_infrastructure', 'own_stand_requirements',
'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',
@@ -66,7 +67,7 @@ function market_management_sanitize_multiline_html(?string $value): ?string
$tokens = preg_split('/(<\/?a\b[^>]*>|<br\s*\/?>|<\/?strong\s*>)/iu', $value, -1, PREG_SPLIT_DELIM_CAPTURE);
if ($tokens === false) {
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
return htmlspecialchars($value, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$result = '';
@@ -89,7 +90,7 @@ function market_management_sanitize_multiline_html(?string $value): ?string
$result .= '</a>';
$anchorOpen = false;
} else {
$result .= htmlspecialchars($token, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$result .= htmlspecialchars($token, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
continue;
}
@@ -103,7 +104,7 @@ function market_management_sanitize_multiline_html(?string $value): ?string
$anchorOpen = true;
continue;
}
$result .= htmlspecialchars($token, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$result .= htmlspecialchars($token, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
if ($anchorOpen) {
$result .= '</a>';
@@ -352,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.');
}
@@ -498,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',
};
@@ -883,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',
@@ -922,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.');
@@ -1029,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,
@@ -1106,7 +1156,6 @@ function market_management_get_occurrence(PDO $pdo, int $occurrenceId, bool $inc
break;
}
}
market_management_sanitize_occurrence_payload($occurrence);
$costSummary = $pdo->prepare(
'SELECT COALESCE(SUM(gross_amount), 0) AS gross_amount,
COALESCE(SUM(net_amount), 0) AS net_amount,
@@ -1124,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;
}
@@ -1139,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',
+92 -31
View File
@@ -87,6 +87,20 @@ function auth_market_status_signal_class(string $status): string
][$status] ?? '';
}
function auth_large_table_default_width(int $columnCount): string
{
return rtrim(rtrim(number_format(100 / max(1, $columnCount), 4, '.', ''), '0'), '.') . '%';
}
function auth_large_table_resize_handle(string $label, int $columnIndex, int $columnCount): string
{
if ($columnIndex >= $columnCount - 1) {
return '';
}
return '<span class="sg-large-table__resize-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Breite von ' . auth_escape_html($label) . ' anpassen" data-large-table-resize-handle="' . $columnIndex . '"></span>';
}
function auth_render_markets_large_table(array $marketTable): string
{
$rows = $marketTable['rows'] ?? $marketTable;
@@ -107,7 +121,8 @@ function auth_render_markets_large_table(array $marketTable): string
$html = [];
$html[] = '<section class="sg-card-list-page" aria-label="Marktdurchführungen">';
$html[] = '<article class="sg-card" data-component="basic-card"><div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-component-part="card-body"><div class="sg-component-row sg-basic-card__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" aria-haspopup="dialog" aria-expanded="false" data-component="button" data-component-state="active" data-market-occurrence-create>Neue Marktdurchführung</button></div></div></article>';
$html[] = '<article class="sg-card sg-large-table sg-large-table--markets" data-component="large-table" data-markets-large-table="true" data-markets-search="' . auth_escape_html($search) . '" data-markets-sort-column="' . auth_escape_html($sortColumn) . '" data-markets-sort-direction="' . auth_escape_html($sortDirection) . '" data-markets-limit="' . $limit . '" data-markets-page-size="' . $pageSize . '" role="table" aria-label="Marktdurchführungen">';
$html[] = '<article class="sg-card sg-large-table sg-large-table--markets" data-component="large-table" data-column-resize="true" data-column-resize-key="market-occurrences" data-column-min-width="8rem" data-markets-large-table="true" data-markets-search="' . auth_escape_html($search) . '" data-markets-sort-column="' . auth_escape_html($sortColumn) . '" data-markets-sort-direction="' . auth_escape_html($sortDirection) . '" data-markets-limit="' . $limit . '" data-markets-page-size="' . $pageSize . '" role="table" aria-label="Marktdurchführungen">';
$html[] = '<div class="sg-large-table__viewport" data-component-part="large-table-viewport">';
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header">';
$html[] = '<div class="sg-strong">Marktdurchführungen</div>';
$html[] = '<span class="sg-search-field-row" data-component="search-field">';
@@ -123,10 +138,10 @@ function auth_render_markets_large_table(array $marketTable): string
$iconDirection = $isActive ? strtolower($sortDirection) : 'ascending';
$targetDirection = $isActive ? ($sortDirection === 'ASC' ? 'DESC' : 'ASC') : ($column === 'earliest_date' ? 'ASC' : 'ASC');
$ariaLabel = $isActive ? $label . ' ' . ($sortDirection === 'ASC' ? 'aufsteigend sortiert' : 'absteigend sortiert') : $label . ' sortieren';
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="' . $headerDirection . '" data-sort-key="' . $headerIndex . '"><button class="sg-large-table__sort-button" type="button" aria-label="' . auth_escape_html($ariaLabel) . '" data-markets-sort-button="true" data-markets-sort-column="' . auth_escape_html($column) . '" data-markets-sort-direction="' . auth_escape_html($targetDirection) . '"><span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span><span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="' . auth_escape_html($iconDirection) . '"></span></button></div>';
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="' . $headerDirection . '" data-sort-key="' . $headerIndex . '" data-default-width="' . auth_large_table_default_width(6) . '"><button class="sg-large-table__sort-button" type="button" aria-label="' . auth_escape_html($ariaLabel) . '" data-markets-sort-button="true" data-markets-sort-column="' . auth_escape_html($column) . '" data-markets-sort-direction="' . auth_escape_html($targetDirection) . '"><span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span><span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="' . auth_escape_html($iconDirection) . '"></span></button>' . auth_large_table_resize_handle($label, $headerIndex, 6) . '</div>';
$headerIndex++;
}
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader">Verkaufte Produkte</div>';
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" data-default-width="' . auth_large_table_default_width(6) . '"><span class="sg-large-table__sort-label">Verkaufte Produkte</span></div>';
$html[] = '</div>';
foreach ($rows as $index => $row) {
@@ -159,9 +174,9 @@ function auth_render_markets_large_table(array $marketTable): string
}
if ($hasMore) {
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--load-more" role="row" data-component-part="large-table-row"><div class="sg-large-table__cell sg-large-table__cell--load-more" role="cell"><div class="sg-navigation-card-layout sg-large-table__load-more-layout"><div class="sg-navigation-card-block"><article class="sg-card" data-component="card" data-pattern="navigation-card" aria-label="Mehr laden"><div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment"><div class="sg-navigation-card-center"><a class="sg-hyperlink" href="#" data-component="hyperlink" data-markets-load-more-trigger="true">Mehr laden</a></div></div></article></div></div></div></div>';
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--load-more" role="row" data-component-part="large-table-row"><div class="sg-large-table__cell sg-large-table__cell--load-more" role="cell"><article class="sg-card sg-large-table__load-more-card" data-component="card" data-pattern="navigation-card" aria-label="Mehr laden"><div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment"><div class="sg-navigation-card-center"><a class="sg-hyperlink" href="#" data-component="hyperlink" data-markets-load-more-trigger="true">Mehr laden</a></div></div></article></div></div>';
}
$html[] = '</article></section>';
$html[] = '</div></article></section>';
return implode('', $html);
}
@@ -180,14 +195,35 @@ function auth_render_market_definitions_content(array $marketTable): string
$rows = $marketTable['rows'] ?? $marketTable;
$categories = $marketTable['categories'] ?? [];
$organizers = $marketTable['organizers'] ?? [];
$sortColumn = (string) ($marketTable['sort_column'] ?? 'name');
$sortDirection = strtoupper((string) ($marketTable['sort_direction'] ?? 'ASC')) === 'DESC' ? 'DESC' : 'ASC';
$html = [];
$html[] = '<div data-market-definitions-content="true" data-market-definitions-csrf-token="' . auth_escape_html(auth_csrf_token()) . '"><section class="sg-card-list-page" aria-label="Märkte">';
$html[] = '<article class="sg-card" data-component="basic-card"><div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-component-part="card-body"><div class="sg-component-row sg-basic-card__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" aria-haspopup="dialog" aria-expanded="false" data-component="button" data-market-definition-create>Neuer Markt</button></div></div></article>';
$html[] = '<article class="sg-card sg-large-table sg-large-table--markets" data-component="large-table" data-market-definitions-table="true" role="table" aria-label="Märkte">';
$html[] = '<article class="sg-card sg-large-table sg-large-table--markets" data-component="large-table" data-column-resize="true" data-column-resize-key="market-definitions" data-column-min-width="8rem" data-market-definitions-table="true" role="table" aria-label="Märkte">';
$html[] = '<div class="sg-large-table__viewport" data-component-part="large-table-viewport">';
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header"><div class="sg-strong">Märkte</div></div>';
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--header" role="row" data-component-part="large-table-header-row">';
foreach (['Markt', 'Kategorie', 'Veranstalter', 'Website', 'Durchführungen', 'Status'] as $label) {
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader">' . auth_escape_html($label) . '</div>';
$definitionColumns = [
'name' => 'Markt',
'category_name' => 'Kategorie',
'organizer_name' => 'Veranstalter',
'website_url' => 'Website',
'occurrence_count' => 'Durchführungen',
'status' => 'Status',
];
$definitionColumnIndex = 0;
foreach ($definitionColumns as $column => $label) {
$isActive = $column === $sortColumn;
$headerDirection = $isActive ? strtolower($sortDirection) : 'none';
$iconDirection = $isActive && $sortDirection === 'DESC' ? 'descending' : 'ascending';
$targetDirection = $isActive && $sortDirection === 'ASC' ? 'DESC' : 'ASC';
$ariaLabel = $isActive
? $label . ' ' . ($sortDirection === 'ASC' ? 'aufsteigend sortiert' : 'absteigend sortiert')
: $label . ' sortieren';
$sortButton = '<button class="sg-large-table__sort-button" type="button" aria-label="' . auth_escape_html($ariaLabel) . '" data-market-definition-sort data-market-definition-sort-column="' . auth_escape_html($column) . '" data-market-definition-sort-direction="' . auth_escape_html($targetDirection) . '"><span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span><span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="' . auth_escape_html($iconDirection) . '"></span></button>';
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="' . auth_escape_html($headerDirection) . '" data-sort-key="' . $definitionColumnIndex . '" data-default-width="' . auth_large_table_default_width(count($definitionColumns)) . '">' . $sortButton . auth_large_table_resize_handle($label, $definitionColumnIndex, count($definitionColumns)) . '</div>';
$definitionColumnIndex++;
}
$html[] = '</div>';
foreach ($rows as $index => $row) {
@@ -210,7 +246,7 @@ function auth_render_market_definitions_content(array $marketTable): string
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html($status) . '</div>';
$html[] = '</div>';
}
$html[] = '</article></section>';
$html[] = '</div></article></section>';
$html[] = '<div class="sg-card-list-page-drawer-backdrop" aria-hidden="true" data-market-definition-drawer-backdrop="true" data-open="false"></div>';
$html[] = '<aside class="sg-card-list-page-drawer" aria-label="Markt-Details" aria-hidden="true" data-market-definition-drawer="true" data-open="false"><div class="sg-card-list-page-drawer__content" data-market-definition-drawer-content><article class="sg-card sg-card--content-card" data-component="content-card" data-shared-editable-action-card data-market-definition-card><header class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen sg-object-card__header"><h2 class="sg-strong sg-text-on-dark" data-market-definition-read-title>Markt</h2><div class="sg-component-row"><button class="sg-interaction-element sg-button sg-button--active" type="button" data-market-definition-edit data-shared-editable-card-edit>Bearbeiten</button></div></header><div class="sg-card-segment sg-card-segment--body sg-card-segment--white" data-market-definition-read-card data-shared-editable-card-read><dl class="market-detail-grid"><div class="market-detail-row"><dt>Marktname</dt><dd data-market-definition-read-name></dd></div><div class="market-detail-row"><dt>Kategorie/Marktart</dt><dd data-market-definition-read-category></dd></div><div class="market-detail-row"><dt>Veranstalter</dt><dd data-market-definition-read-organizer></dd></div><div class="market-detail-row"><dt>Website</dt><dd data-market-definition-read-website></dd></div></dl></div><div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-market-definition-create-card data-shared-editable-card-form hidden><h2 class="sg-strong" data-market-definition-form-title>Markt bearbeiten</h2><div class="sg-form-sections-card-wrapper" data-pattern="form-sections"><form class="sg-form-sections-card" data-market-definition-form><div class="sg-form-sections-card__body" data-pattern-part="form-body"><div class="sg-form-sections-card__section"><div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Marktname<span class="market-required-field-indicator" aria-hidden="true">*</span></span><input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" name="name" required data-market-definition-name></label></div><div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Kategorie/Marktart<span class="market-required-field-indicator" aria-hidden="true">*</span></span><select class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable sg-pulldown" name="category_id" required data-market-definition-category><option value="">Bitte auswählen</option>';
foreach ($categories as $category) {
@@ -244,7 +280,8 @@ function auth_render_bestellungen_large_table(array $bestellungenTable): string
$html = [];
$html[] = '<section class="sg-card-list-page" aria-label="Bestellungen">';
$html[] = '<article class="sg-card sg-large-table sg-large-table--bestellungen" style="margin-top: var(--spacing-large);" data-component="large-table" data-bestellungen-large-table="true" data-bestellungen-search="' . auth_escape_html($search) . '" data-bestellungen-sort-column="' . auth_escape_html($sortColumn) . '" data-bestellungen-sort-direction="' . auth_escape_html($sortDirection) . '" data-bestellungen-limit="' . $limit . '" data-bestellungen-page-size="' . $pageSize . '" role="table" aria-label="Bestellungen">';
$html[] = '<article class="sg-card sg-large-table sg-large-table--bestellungen" style="margin-top: var(--spacing-large);" data-component="large-table" data-column-resize="true" data-column-resize-key="orders" data-column-min-width="8rem" data-bestellungen-large-table="true" data-bestellungen-search="' . auth_escape_html($search) . '" data-bestellungen-sort-column="' . auth_escape_html($sortColumn) . '" data-bestellungen-sort-direction="' . auth_escape_html($sortDirection) . '" data-bestellungen-limit="' . $limit . '" data-bestellungen-page-size="' . $pageSize . '" role="table" aria-label="Bestellungen">';
$html[] = '<div class="sg-large-table__viewport" data-component-part="large-table-viewport">';
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header">';
$html[] = '<div class="sg-strong">Bestellungen</div>';
$html[] = '<span class="sg-search-field-row" data-component="search-field">';
@@ -269,11 +306,11 @@ function auth_render_bestellungen_large_table(array $bestellungenTable): string
: ($column === 'order_date' ? 'DESC' : 'ASC');
$amountHeaderClass = $column === 'total_amount' ? ' sg-large-table__cell--amount' : '';
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header' . $amountHeaderClass . '" role="columnheader" aria-sort="' . $headerDirection . '" data-sort-key="' . $headerIndex . '">';
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header' . $amountHeaderClass . '" role="columnheader" aria-sort="' . $headerDirection . '" data-sort-key="' . $headerIndex . '" data-default-width="' . auth_large_table_default_width(4) . '">';
$html[] = '<button class="sg-large-table__sort-button" type="button" aria-label="' . auth_escape_html($ariaLabel) . '" data-bestellungen-sort-button="true" data-bestellungen-sort-column="' . auth_escape_html($column) . '" data-bestellungen-sort-direction="' . auth_escape_html($targetDirection) . '">';
$html[] = '<span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span>';
$html[] = '<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="' . auth_escape_html($iconDirection) . '"></span>';
$html[] = '</button>';
$html[] = '</button>' . auth_large_table_resize_handle($label, $headerIndex, 4);
$html[] = '</div>';
$headerIndex++;
}
@@ -321,9 +358,7 @@ function auth_render_bestellungen_large_table(array $bestellungenTable): string
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--load-more" role="row" data-component-part="large-table-row" data-large-table-load-more-row="true">';
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--load-more" role="cell">';
$html[] = '<div class="sg-navigation-card-layout sg-large-table__load-more-layout">';
$html[] = '<div class="sg-navigation-card-block">';
$html[] = '<article class="sg-card" data-component="card" data-pattern="navigation-card" aria-label="Mehr laden">';
$html[] = '<article class="sg-card sg-large-table__load-more-card" data-component="card" data-pattern="navigation-card" aria-label="Mehr laden">';
$html[] = '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment">';
$html[] = '<div class="sg-navigation-card-center">';
$html[] = '<a class="sg-hyperlink" href="' . auth_escape_html($loadMoreUrl) . '" data-component="hyperlink" data-large-table-load-more-trigger="true">Mehr laden</a>';
@@ -331,11 +366,9 @@ function auth_render_bestellungen_large_table(array $bestellungenTable): string
$html[] = '</div>';
$html[] = '</article>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</article>';
$html[] = '</section>';
@@ -388,11 +421,12 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
return ['text' => $text, 'warning' => $warning, 'mhdWarning' => $mhdWarning];
};
$html = [];
$html[] = '<article class="sg-card sg-large-table sg-large-table--lager" data-component="large-table" data-lager-table="true" data-lager-table-key="' . auth_escape_html($tableKey) . '" role="table" aria-label="' . auth_escape_html($title) . '">';
$html[] = '<article class="sg-card sg-large-table sg-large-table--lager" data-component="large-table" data-column-resize="true" data-column-resize-key="lager-' . auth_escape_html($tableKey) . '" data-column-min-width="8rem" data-lager-table="true" data-lager-table-key="' . auth_escape_html($tableKey) . '" role="table" aria-label="' . auth_escape_html($title) . '">';
$html[] = '<div class="sg-large-table__viewport" data-component-part="large-table-viewport">';
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header"><div class="sg-strong">' . auth_escape_html($title) . '</div></div>';
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--header" role="row" data-component-part="large-table-header-row">';
foreach ($columns as $label) {
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader"><span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span></div>';
foreach ($columns as $index => $label) {
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" data-default-width="' . auth_large_table_default_width(count($columns)) . '"><span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span>' . auth_large_table_resize_handle($label, $index, count($columns)) . '</div>';
}
$html[] = '</div>';
@@ -474,6 +508,7 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</article>';
return implode('', $html);
}
@@ -739,7 +774,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
echo '<meta charset="UTF-8">';
echo '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
echo '<title>Naurua ERP</title>';
echo '<link rel="stylesheet" href="/assets/styles.css">';
echo '<link rel="stylesheet" href="/assets/styles.css?v=' . filemtime(__DIR__ . '/../../../../public/assets/styles.css') . '">';
echo '<link rel="stylesheet" href="/assets/styles.portal.css?v=' . filemtime(__DIR__ . '/../../../../public/assets/styles.portal.css') . '">';
echo '</head>';
echo '<body>';
@@ -833,7 +868,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
echo '</select>';
echo '</label>';
echo '<label class="sg-labeled-input-row"><span class="sg-label">Betrag (CHF)</span>';
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="number" id="otc-payment-amount-1" min="0.01" step="0.01" value="0.00" data-otc-payment-amount>';
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="number" id="otc-payment-amount-1" min="0.01" step="0.01" placeholder="0.00" data-otc-payment-amount>';
echo '</label>';
echo '</div>';
echo '</div>';
@@ -930,12 +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]; }";
@@ -1159,11 +1197,19 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
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; };";
@@ -1172,14 +1218,14 @@ 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]');";
@@ -1194,8 +1240,15 @@ 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>'; } } };";
@@ -1213,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(); } });";
@@ -1302,6 +1355,7 @@ 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) => { 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; };";
@@ -1312,6 +1366,7 @@ JS;
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(); 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(); });";
@@ -1403,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'; }";
@@ -1635,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();";
@@ -1822,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);";
@@ -1850,6 +1910,7 @@ JS;
echo " const result = await response.json();";
echo " if (response.ok && result.ok) {";
echo " submissionSucceeded = true;";
echo " document.dispatchEvent(new CustomEvent('otc-order-created', { detail: { orderId: result.orderId } }));";
echo " if (successEl) {";
echo " successEl.textContent = result.externalRef ? 'Bestellung erfolgreich erfasst! Bestellnummer: ' + result.externalRef : successDefaultText;";
echo " successEl.classList.remove('hidden');";
+88
View File
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../modules/shared/db.php';
require_once __DIR__ . '/../../modules/erp/lager/mcp.php';
final class ErpLagerMcpJsonRpcException extends RuntimeException
{
public function __construct(private int $rpcCode, string $message, ?Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
}
public function rpcCode(): int
{
return $this->rpcCode;
}
}
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
$env = expand_env_values(parse_env_file(__DIR__ . '/../../.env'));
$request = [];
try {
erp_lager_mcp_authorize($env);
$pdo = connect_database($env);
$body = file_get_contents('php://input');
$request = json_decode($body === false ? '' : $body, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($request) || ($request['jsonrpc'] ?? '') !== '2.0') {
throw new InvalidArgumentException('Ungültige JSON-RPC-Anfrage.');
}
if (array_key_exists('params', $request) && !is_array($request['params'])) {
throw new ErpLagerMcpJsonRpcException(-32602, 'Parameter müssen als Objekt oder Array übergeben werden.');
}
$id = $request['id'] ?? null;
$method = (string) ($request['method'] ?? '');
$params = is_array($request['params'] ?? null) ? $request['params'] : [];
if ($method === 'initialize') {
$result = [
'protocolVersion' => (string) ($params['protocolVersion'] ?? '2025-03-26'),
'capabilities' => ['tools' => ['listChanged' => false]],
'serverInfo' => ['name' => 'naurua-erp-lager', 'version' => '1.0.0'],
];
} elseif ($method === 'notifications/initialized') {
http_response_code(202);
exit;
} elseif ($method === 'tools/list') {
$result = ['tools' => erp_lager_mcp_tools()];
} elseif ($method === 'tools/call') {
$tool = (string) ($params['name'] ?? '');
$arguments = is_array($params['arguments'] ?? null) ? $params['arguments'] : [];
if (array_key_exists('arguments', $params) && !is_array($params['arguments'])) {
throw new ErpLagerMcpJsonRpcException(-32602, 'Tool-Argumente müssen als Objekt übergeben werden.');
}
try {
$result = [
'content' => [[
'type' => 'text',
'text' => json_encode(erp_lager_mcp_call($pdo, $tool, $arguments), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
]],
'isError' => false,
];
} catch (InvalidArgumentException $e) {
throw new ErpLagerMcpJsonRpcException(-32602, $e->getMessage(), $e);
}
} else {
throw new ErpLagerMcpJsonRpcException(-32601, 'Unbekannte JSON-RPC-Methode: ' . $method);
}
echo json_encode(['jsonrpc' => '2.0', 'id' => $id, 'result' => $result], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
} catch (Throwable $e) {
$isAuthError = $e instanceof RuntimeException && str_contains($e->getMessage(), 'Token');
$isJsonRpcError = $e instanceof ErpLagerMcpJsonRpcException;
http_response_code($isJsonRpcError ? 200 : ($isAuthError ? 401 : 400));
echo json_encode([
'jsonrpc' => '2.0',
'id' => $request['id'] ?? null,
'error' => [
'code' => $isJsonRpcError ? $e->rpcCode() : -32600,
'message' => $e->getMessage(),
],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
+7
View File
@@ -63,6 +63,13 @@ if ($requestMethod === 'POST') {
$day = market_management_delete_day($pdo, $id);
echo json_encode(['ok' => true, 'day' => $day]);
exit;
} elseif (($payload['action'] ?? '') === 'update_checklist') {
$checklist = market_management_update_occurrence_checklist($pdo, $id, [
'items' => $payload['items'] ?? null,
'comment' => $payload['comment'] ?? null,
]);
echo json_encode(['ok' => true, 'checklist' => $checklist], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
} elseif ($scope === 'day') {
market_management_update_day($pdo, $id, $fields);
} elseif (in_array($scope, ['occurrence', 'status', 'overview', 'participation', 'infrastructure', 'travel', 'notes'], true)) {
+166 -148
View File
@@ -64,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);
@@ -79,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);
@@ -1820,33 +1818,33 @@ 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%);
}
@@ -1861,33 +1859,33 @@ section + section {
}
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="bottom"]::before {
top: calc(var(--spacing-small) * -1);
top: calc(var(--spacing-small) * -2);
left: var(--tooltip-arrow-offset);
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-panel[data-tooltip-portal="true"][data-tooltip-placement="top"]::before {
bottom: calc(var(--spacing-small) * -1);
bottom: calc(var(--spacing-small) * -2);
left: var(--tooltip-arrow-offset);
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-panel[data-tooltip-portal="true"][data-tooltip-placement="left"]::before {
top: var(--tooltip-arrow-offset);
right: calc(var(--spacing-small) * -1);
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
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) * -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%);
}
@@ -2641,6 +2639,11 @@ section + section {
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%;
}
@@ -2667,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;
@@ -2991,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 {
@@ -3031,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"] {
@@ -3070,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 {
@@ -3094,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 {
@@ -4946,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%;
@@ -4963,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;
}
@@ -4994,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;
+2 -2
View File
@@ -1,7 +1,7 @@
{
"styleguideVersion": "2026.05.18.2",
"styleguideCommit": "519ed6b",
"syncedAtUtc": "2026-09-24T11:09:21Z",
"styleguideCommit": "08fc673",
"syncedAtUtc": "2026-10-14T11:46:02Z",
"sourceRepo": "/Users/mathias/Documents/Dokumente Chouchou/Codebases/Styleguide",
"mirroredDocsPath": "docs/styleguide"
}
+166 -148
View File
@@ -64,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);
@@ -79,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);
@@ -1820,33 +1818,33 @@ 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%);
}
@@ -1861,33 +1859,33 @@ section + section {
}
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="bottom"]::before {
top: calc(var(--spacing-small) * -1);
top: calc(var(--spacing-small) * -2);
left: var(--tooltip-arrow-offset);
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-panel[data-tooltip-portal="true"][data-tooltip-placement="top"]::before {
bottom: calc(var(--spacing-small) * -1);
bottom: calc(var(--spacing-small) * -2);
left: var(--tooltip-arrow-offset);
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-panel[data-tooltip-portal="true"][data-tooltip-placement="left"]::before {
top: var(--tooltip-arrow-offset);
right: calc(var(--spacing-small) * -1);
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
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) * -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%);
}
@@ -2641,6 +2639,11 @@ section + section {
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%;
}
@@ -2667,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;
@@ -2991,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 {
@@ -3031,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"] {
@@ -3070,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 {
@@ -3094,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 {
@@ -4946,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%;
@@ -4963,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;
}
@@ -4994,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;
+86 -5
View File
@@ -303,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) {
@@ -316,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 {
@@ -339,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 {
@@ -431,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;
@@ -458,3 +520,22 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
[data-bestellungen-mobile-detail] .sg-navigation-card-layout {
margin-top: 0;
}
@media (max-width: 767px) {
.market-checklist-edit-row {
flex-direction: row;
align-items: center;
gap: var(--spacing-small);
}
.market-checklist-edit-row .sg-label {
min-width: 0;
flex: 0 0 auto;
margin-right: 0;
margin-bottom: 0;
}
.market-checklist-edit-control {
max-width: none;
}
}
+6 -2
View File
@@ -79,16 +79,20 @@ if ($currentUser !== null) {
'page_size' => 20,
];
$marketOccurrencesTable['has_more'] = count($marketOccurrencesTable['rows']) >= $marketLimit;
$marketDefinitionSort = (string) ($_GET['market_definition_sort'] ?? 'name');
$marketDefinitionDirection = strtoupper((string) ($_GET['market_definition_dir'] ?? 'ASC'));
$marketDefinitionsTable = [
'rows' => market_management_search_markets($pdo, [
'include_archived' => false,
'sort' => 'name',
'sort_direction' => 'ASC',
'sort' => $marketDefinitionSort,
'sort_direction' => $marketDefinitionDirection,
'limit' => 100,
'offset' => 0,
]),
'categories' => market_management_list_categories($pdo),
'organizers' => market_management_list_organizers($pdo),
'sort_column' => $marketDefinitionSort,
'sort_direction' => $marketDefinitionDirection,
];
if ((string) ($_GET['bestellungen_fragment'] ?? '') === '1') {
echo auth_render_bestellungen_content($bestellungenTable);
+245
View File
@@ -0,0 +1,245 @@
(function initLargeTableColumnResizeModule() {
const STORAGE_PREFIX = 'sg:large-table:column-widths:';
const STORAGE_VERSION = 3;
const DEFAULT_MIN_WIDTH = 128;
const KEYBOARD_STEP = 8;
const KEYBOARD_LARGE_STEP = 40;
let activeResize = null;
const getTables = (root) => root.querySelectorAll('[data-component="large-table"][data-column-resize="true"]');
const getColumnHeaders = (table) => Array.from(
table.querySelectorAll('[data-component-part="large-table-header-row"] [role="columnheader"]'),
);
const getMinWidth = (table) => {
const configured = table.dataset.columnMinWidth;
const value = configured || getComputedStyle(table).getPropertyValue('--large-table-column-min-width').trim();
if (!value) {
return DEFAULT_MIN_WIDTH;
}
const probe = document.createElement('span');
probe.style.cssText = `position:absolute;width:${value};visibility:hidden;`;
table.appendChild(probe);
const width = probe.getBoundingClientRect().width;
probe.remove();
return width > 0 ? width : DEFAULT_MIN_WIDTH;
};
const parseDefaultWidth = (rawValue, tableWidth) => {
if (!rawValue) {
return null;
}
const value = Number.parseFloat(rawValue);
if (!Number.isFinite(value) || value <= 0) {
return null;
}
if (rawValue.trim().endsWith('%')) {
return tableWidth * value / 100;
}
if (rawValue.trim().endsWith('fr')) {
return value;
}
return value;
};
const normalizeRatios = (values) => {
const safeValues = values.map((value) => (Number.isFinite(value) && value > 0 ? value : 0));
const total = safeValues.reduce((sum, value) => sum + value, 0);
if (total <= 0) {
return Array(values.length).fill(1 / values.length);
}
return safeValues.map((value) => value / total);
};
const getDefaultRatios = (table, headers, minWidth) => {
const tableWidth = Math.max(table.querySelector('[data-component-part="large-table-viewport"]')?.clientWidth || 0, minWidth * headers.length);
const configured = headers.map((header) => parseDefaultWidth(header.dataset.defaultWidth, tableWidth));
if (configured.some((width) => width === null)) {
return Array(headers.length).fill(1 / headers.length);
}
return normalizeRatios(configured);
};
const getStorageKey = (table) => `${STORAGE_PREFIX}${table.dataset.columnResizeKey || ''}`;
const readStoredRatios = (table, count) => {
const key = getStorageKey(table);
if (!table.dataset.columnResizeKey) {
return null;
}
try {
if (!window.localStorage) {
return null;
}
const stored = JSON.parse(window.localStorage.getItem(key));
if (stored?.version !== STORAGE_VERSION || !Array.isArray(stored.ratios) || stored.ratios.length !== count) {
return null;
}
if (stored.ratios.some((ratio) => !Number.isFinite(ratio) || ratio <= 0)) {
return null;
}
return normalizeRatios(stored.ratios);
} catch (error) {
return null;
}
};
const writeStoredRatios = (table, ratios) => {
if (!table.dataset.columnResizeKey) {
return;
}
try {
if (!window.localStorage) {
return;
}
window.localStorage.setItem(getStorageKey(table), JSON.stringify({
version: STORAGE_VERSION,
ratios: normalizeRatios(ratios),
}));
} catch (error) {
// Storage can be unavailable in privacy-restricted browser contexts.
}
};
const updateHandleState = (table, widths, minWidth) => {
const handles = table.querySelectorAll('[data-large-table-resize-handle]');
handles.forEach((handle) => {
const index = Number.parseInt(handle.dataset.largeTableResizeHandle, 10);
const current = widths[index];
const maximum = Math.max(minWidth, widths[index] + widths[index + 1] - minWidth);
if (table.id) {
handle.setAttribute('aria-controls', table.id);
}
handle.setAttribute('aria-valuemin', String(Math.round(minWidth)));
handle.setAttribute('aria-valuemax', String(Math.round(maximum)));
handle.setAttribute('aria-valuenow', String(Math.round(current)));
handle.setAttribute('aria-valuetext', `${Math.round(current)} Pixel`);
});
};
const getRenderedWidths = (table, count, minWidth) => {
const headers = getColumnHeaders(table);
const widths = headers.map((header) => header.getBoundingClientRect().width);
if (widths.length === count && widths.every((width) => width > 0)) {
return widths;
}
return Array(count).fill(minWidth);
};
const applyRatios = (table, ratios, minWidth) => {
const safeRatios = normalizeRatios(ratios);
const columnTemplate = safeRatios
.map((ratio) => `minmax(${Math.ceil(minWidth)}px, ${ratio}fr)`)
.join(' ');
table.style.setProperty('--large-table-grid-template-columns', columnTemplate);
table.style.setProperty('--large-table-grid-min-width', `${Math.ceil(minWidth * safeRatios.length)}px`);
table._largeTableColumnRatios = safeRatios;
table._largeTableColumnWidths = getRenderedWidths(table, safeRatios.length, minWidth);
updateHandleState(table, table._largeTableColumnWidths, minWidth);
};
const resizeColumn = (table, index, delta, persist, baseWidths = table._largeTableColumnWidths) => {
const widths = [...baseWidths];
const minWidth = getMinWidth(table);
const nextLeft = Math.max(minWidth, Math.min(widths[index] + widths[index + 1] - minWidth, widths[index] + delta));
const appliedDelta = nextLeft - widths[index];
widths[index] = nextLeft;
widths[index + 1] -= appliedDelta;
applyRatios(table, widths.map((width) => width / widths.reduce((sum, value) => sum + value, 0)), minWidth);
if (persist) {
writeStoredRatios(table, table._largeTableColumnRatios);
}
};
const initTable = (table) => {
const headers = getColumnHeaders(table);
if (headers.length < 2) {
return;
}
const minWidth = getMinWidth(table);
const stored = readStoredRatios(table, headers.length);
applyRatios(table, stored || getDefaultRatios(table, headers, minWidth), minWidth);
if (table.dataset.largeTableResizeInitialized === 'true') {
return;
}
table.dataset.largeTableResizeInitialized = 'true';
table.addEventListener('pointerdown', (event) => {
const handle = event.target.closest('[data-large-table-resize-handle]');
if (!handle || !table.contains(handle)) {
return;
}
event.preventDefault();
activeResize = {
table,
handle,
index: Number.parseInt(handle.dataset.largeTableResizeHandle, 10),
startX: event.clientX,
startWidths: [...table._largeTableColumnWidths],
};
handle.dataset.largeTableResizeActive = 'true';
table.dataset.columnResizeState = 'resizing';
handle.setPointerCapture?.(event.pointerId);
});
table.addEventListener('pointermove', (event) => {
if (!activeResize || activeResize.table !== table) {
return;
}
resizeColumn(
table,
activeResize.index,
event.clientX - activeResize.startX,
false,
activeResize.startWidths,
);
});
table.addEventListener('pointerup', () => {
if (!activeResize || activeResize.table !== table) {
return;
}
writeStoredRatios(table, table._largeTableColumnRatios);
activeResize.handle.dataset.largeTableResizeActive = 'false';
table.dataset.columnResizeState = 'idle';
activeResize = null;
});
if (typeof ResizeObserver === 'function') {
table._largeTableResizeObserver = new ResizeObserver(() => {
applyRatios(table, table._largeTableColumnRatios, getMinWidth(table));
});
table._largeTableResizeObserver.observe(table.querySelector('[data-component-part="large-table-viewport"]'));
}
table.addEventListener('keydown', (event) => {
const handle = event.target.closest('[data-large-table-resize-handle]');
if (!handle || !table.contains(handle)) {
return;
}
const index = Number.parseInt(handle.dataset.largeTableResizeHandle, 10);
const step = event.shiftKey ? KEYBOARD_LARGE_STEP : KEYBOARD_STEP;
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
event.preventDefault();
resizeColumn(table, index, event.key === 'ArrowRight' ? step : -step, true);
} else if (event.key === 'Home' || event.key === 'End') {
event.preventDefault();
const minWidth = getMinWidth(table);
const current = table._largeTableColumnWidths[index];
const target = event.key === 'Home' ? minWidth : current + table._largeTableColumnWidths[index + 1] - minWidth;
resizeColumn(table, index, target - current, true);
}
});
};
window.sgInitLargeTableColumnResize = (options = {}) => {
const root = options.root || document;
getTables(root).forEach(initTable);
};
document.addEventListener('DOMContentLoaded', () => window.sgInitLargeTableColumnResize(), { once: true });
})();
+114 -2
View File
@@ -2,9 +2,87 @@
'use strict';
const cardSelector = '[data-shared-editable-action-card]';
const scrollRestoreStates = new WeakMap();
const getCard = (element) => element.closest(cardSelector);
const setToggleEnabled = (card, enabled) => {
const toggle = card?.querySelector('[data-shared-editable-card-toggle]');
if (!toggle) {
return;
}
toggle.disabled = !enabled;
toggle.setAttribute('aria-disabled', String(!enabled));
toggle.classList.toggle('sg-button--inactive', !enabled);
};
const setVisibility = (card, visible) => {
if (!card || !card.hasAttribute('data-shared-editable-card-collapsible')) {
return;
}
const readView = card.querySelector('[data-shared-editable-card-read]');
const toggle = card.querySelector('[data-shared-editable-card-toggle]');
card.dataset.sharedEditableCardVisible = String(visible);
card.classList.toggle('shared-editable-action-card--collapsed', !visible);
if (readView && card.querySelector('[data-shared-editable-card-form]')?.hidden !== false) {
readView.hidden = !visible;
}
if (toggle) {
toggle.textContent = visible ? 'Verbergen' : 'Anzeigen';
toggle.setAttribute('aria-expanded', String(visible));
}
};
const isVisible = (card) => card?.dataset.sharedEditableCardVisible === 'true';
const getScrollContainer = (element) => {
let candidate = element?.parentElement || null;
while (candidate && candidate !== document.body) {
const style = window.getComputedStyle(candidate);
const scrollable = /(auto|scroll|overlay)/.test(style.overflowY);
if (scrollable && candidate.scrollHeight > candidate.clientHeight) {
return candidate;
}
candidate = candidate.parentElement;
}
return document.scrollingElement || document.documentElement;
};
const captureScrollPosition = (root, card) => {
const container = getScrollContainer(card);
scrollRestoreStates.set(root, {
card,
container,
top: container.scrollTop,
left: container.scrollLeft,
timer: null,
});
};
const restoreScrollPosition = (root) => {
const state = scrollRestoreStates.get(root);
if (!state) {
return;
}
if (state.timer) {
window.clearTimeout(state.timer);
}
state.timer = window.setTimeout(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (state.container?.isConnected) {
state.container.scrollTo({ top: state.top, left: state.left, behavior: 'auto' });
}
scrollRestoreStates.delete(root);
});
});
}, 100);
};
const getDirectDeleteElement = (card, selector) => Array.from(card?.children || [])
.find((child) => child.matches(selector)) || null;
@@ -17,6 +95,15 @@
const editView = card.querySelector('[data-shared-editable-card-form]');
const editButtons = card.querySelectorAll('[data-shared-editable-card-edit]');
if (card.hasAttribute('data-shared-editable-card-collapsible')) {
if (enabled) {
setVisibility(card, true);
} else if (!card.dataset.sharedEditableCardVisible) {
setVisibility(card, !card.hasAttribute('data-shared-editable-card-collapsed'));
}
setToggleEnabled(card, !enabled);
}
if (readView) {
readView.hidden = enabled;
}
@@ -28,6 +115,10 @@
editButton.setAttribute('aria-disabled', String(enabled));
editButton.classList.toggle('sg-button--inactive', enabled);
});
if (card.hasAttribute('data-shared-editable-card-collapsible') && enabled && readView) {
readView.hidden = true;
}
};
const resetDeleteConfirmation = (card) => {
@@ -91,9 +182,21 @@
root.dataset.sharedEditableActionCardsInitialized = 'true';
root.querySelectorAll(cardSelector).forEach((card) => setEditMode(card, false));
root.addEventListener('click', (event) => {
const toggle = event.target.closest('[data-shared-editable-card-toggle]');
if (toggle && root.contains(toggle)) {
const card = getCard(toggle);
if (card?.querySelector('[data-shared-editable-card-form]')?.hidden === false) {
return;
}
setVisibility(card, !isVisible(card));
return;
}
const edit = event.target.closest('[data-shared-editable-card-edit]');
if (edit && root.contains(edit)) {
setEditMode(getCard(edit), true);
const card = getCard(edit);
captureScrollPosition(root, card);
setEditMode(card, true);
return;
}
@@ -102,6 +205,7 @@
const card = getCard(cancel);
resetDeleteConfirmation(card);
setEditMode(card, false);
scrollRestoreStates.delete(root);
return;
}
@@ -117,6 +221,14 @@
}
});
const scrollObserver = new MutationObserver(() => {
const state = scrollRestoreStates.get(root);
if (state && !root.contains(state.card)) {
restoreScrollPosition(root);
}
});
scrollObserver.observe(root, { childList: true, subtree: true });
root.addEventListener('input', (event) => {
const input = event.target.closest('[data-shared-editable-card-delete-input]');
if (input && root.contains(input)) {
@@ -125,5 +237,5 @@
});
};
window.SharedEditableActionCard = { init, resetDeleteConfirmation, setEditMode };
window.SharedEditableActionCard = { init, resetDeleteConfirmation, setEditMode, setToggleEnabled, setVisibility, isVisible };
}());
@@ -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");
@@ -19,7 +19,7 @@ function assert_test(bool $condition, string $message): void
function assert_schema_objects(array $schema, string $path = 'inputSchema'): void
{
assert_test(($schema['type'] ?? null) === 'object', $path . '.type must be object');
assert_test(isset($schema['properties']) && is_object($schema['properties']), $path . '.properties must be an object');
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');
@@ -28,7 +28,10 @@ function assert_schema_objects(array $schema, string $path = 'inputSchema'): voi
}
}
foreach (get_object_vars($schema['properties']) as $propertyName => $propertySchema) {
$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');
@@ -55,6 +58,8 @@ $expected = [
'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',
@@ -64,7 +69,31 @@ $expected = [
];
$tools = market_management_mcp_tools();
assert_test(count($tools) === 21, 'Expected 21 MCP tools, got ' . count($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) {
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../modules/market_management/service.php';
function fail_test(string $message): void
{
fwrite(STDERR, $message . PHP_EOL);
exit(1);
}
function assert_test(bool $condition, string $message): void
{
if (!$condition) {
fail_test($message);
}
}
$plainText = "Test 'A' & <B> \"C\"";
$sanitized = market_management_sanitize_multiline_html($plainText);
assert_test($sanitized === "Test 'A' &amp; &lt;B&gt; \"C\"", 'Plain text must preserve quotes and escape markup characters once');
$link = '<a href="https://erpnaurua.imhochrain.ch/files.php?path=Marktmanagement%2Fvertrag.pdf">Vertrag</a>';
$sanitizedLink = market_management_sanitize_multiline_html($link);
assert_test(str_contains($sanitizedLink, 'class="sg-hyperlink"'), 'Allowed links must remain links');
assert_test(str_contains($sanitizedLink, 'href="https://erpnaurua.imhochrain.ch/files.php?path=Marktmanagement%2Fvertrag.pdf"'), 'Link href must remain valid');
fwrite(STDOUT, "market_management_sanitizer_test: OK\n");
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../modules/market_management/service.php';
function fail_test(string $message): void
{
fwrite(STDERR, $message . PHP_EOL);
exit(1);
}
function assert_test(bool $condition, string $message): void
{
if (!$condition) {
fail_test($message);
}
}
$normalized = market_management_optional_url(['website_url' => 'www.stadtaffoltern.ch'], 'website_url');
assert_test($normalized === 'https://www.stadtaffoltern.ch', 'Website without scheme must be normalized to HTTPS');
$existingScheme = market_management_optional_url(['website_url' => 'http://www.stadtaffoltern.ch'], 'website_url');
assert_test($existingScheme === 'http://www.stadtaffoltern.ch', 'Existing HTTP scheme must be preserved');
try {
market_management_optional_url(['website_url' => 'not a url'], 'website_url');
fail_test('Invalid website URL must be rejected');
} catch (InvalidArgumentException) {
}
fwrite(STDOUT, "market_management_url_test: OK\n");