Compare commits
152
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
732068ad9c | ||
|
|
ea434033f0 | ||
|
|
ebb71b4769 | ||
|
|
8c5668bf3d | ||
|
|
53baf0dfbf | ||
|
|
0c04caeb67 | ||
|
|
8b163c5734 | ||
|
|
a6007b5853 | ||
|
|
386b539f88 | ||
|
|
58c5912c30 | ||
|
|
17e07d525b | ||
|
|
dd019afff6 | ||
|
|
276a496ae8 | ||
|
|
9f10344fa7 | ||
|
|
c25d3f1b3c | ||
|
|
73e2144571 | ||
|
|
3d7f6b1820 | ||
|
|
d0079aa5f7 | ||
|
|
e2e6a40d9a | ||
|
|
99eae4cada | ||
|
|
de10e37a0c | ||
|
|
4758aecec2 | ||
|
|
c4e1c291d8 | ||
|
|
42127b0916 | ||
|
|
d03f653848 | ||
|
|
b1fd16cf06 | ||
|
|
b2cb88e4ff | ||
|
|
ee9bcc85ce | ||
|
|
4f0094a821 | ||
|
|
c2913c1026 | ||
|
|
13420b0e01 | ||
|
|
5567b53581 | ||
|
|
ff0fffccad | ||
|
|
38886e39bd | ||
|
|
25ef8ef7b0 | ||
|
|
6750d4778f | ||
|
|
7fc75eaee6 | ||
|
|
f1b090ee85 | ||
|
|
e6431dbe66 | ||
|
|
23c0cf69d7 | ||
|
|
e4be458433 | ||
|
|
0c8cb05237 | ||
|
|
5d9c058e10 | ||
|
|
af235987ea | ||
|
|
bae293ad94 | ||
|
|
1ad35cdcfd | ||
|
|
c0c3c2600f | ||
|
|
5413bae3ea | ||
|
|
e51f3ab507 | ||
|
|
e1f482ef5c | ||
|
|
f14023bd92 | ||
|
|
d68654e94f | ||
|
|
724e3413e4 | ||
|
|
f77ce71bd6 | ||
|
|
dc52003c01 | ||
|
|
3a0cb81a97 | ||
|
|
313475afec | ||
|
|
6226f2a037 | ||
|
|
2a09401600 | ||
|
|
2d3c9ce4ab | ||
|
|
273fb05833 | ||
|
|
dfe9748ff9 | ||
|
|
1b66cd3add | ||
|
|
a6f9236abf | ||
|
|
36fb17ac32 | ||
|
|
0e71aca89b | ||
|
|
ba36485180 | ||
|
|
0edd9e7b3c | ||
|
|
1fcd6aa245 | ||
|
|
1d0e71c5db | ||
|
|
5825d37e83 | ||
|
|
5b83d10d92 | ||
|
|
95c42b342b | ||
|
|
78cc6390f7 | ||
|
|
9b49db98d4 | ||
|
|
e6bf7be2f9 | ||
|
|
212476c3f6 | ||
|
|
42684f3864 | ||
|
|
e2e46a601b | ||
|
|
a1ac5f34ae | ||
|
|
1bf5b51679 | ||
|
|
b68546f824 | ||
|
|
841e559a70 | ||
|
|
f0af2efe8a | ||
|
|
c9d386badd | ||
|
|
f4834e0e7a | ||
|
|
1921464e27 | ||
|
|
f720782736 | ||
|
|
43598bf65a | ||
|
|
2bfe91695e | ||
|
|
22880b512a | ||
|
|
debee672f8 | ||
|
|
3d60998145 | ||
|
|
481da014bc | ||
|
|
d22395b96c | ||
|
|
aaa9fa67e9 | ||
|
|
ca2facdbc6 | ||
|
|
3d638ab2a1 | ||
|
|
30c11c5ca7 | ||
|
|
2a1144f0aa | ||
|
|
21e0f48fd5 | ||
|
|
9fc85141e0 | ||
|
|
184f50d034 | ||
|
|
6fae2de15f | ||
|
|
e188f932f4 | ||
|
|
71f329170f | ||
|
|
3ba33f85a0 | ||
|
|
1b553a9095 | ||
|
|
e9d157c87e | ||
|
|
47c09a3e4c | ||
|
|
759e337976 | ||
|
|
b15eda6ff1 | ||
|
|
71b27a387d | ||
|
|
2c734bd415 | ||
|
|
20f130f614 | ||
|
|
4eef9d9785 | ||
|
|
361635332c | ||
|
|
a42d7a0718 | ||
|
|
4b112f3a57 | ||
|
|
0de04a8dfa | ||
|
|
bace7b4a05 | ||
|
|
18e89595e0 | ||
|
|
d1129175bc | ||
|
|
c1cb272702 | ||
|
|
b7ff51adc5 | ||
|
|
fdc0f02b91 | ||
|
|
c4b93ff217 | ||
|
|
ca42ded4e2 | ||
|
|
30e44755fe | ||
|
|
2862d7a6e9 | ||
|
|
28784ed6ac | ||
|
|
6a12a43114 | ||
|
|
94d336f090 | ||
|
|
af0206d9cd | ||
|
|
65d2b1405f | ||
|
|
57d8da9577 | ||
|
|
2a754973cd | ||
|
|
e503759197 | ||
|
|
47baeaa5f7 | ||
|
|
0fba652770 | ||
|
|
9064d4eb42 | ||
|
|
f66c22027e | ||
|
|
614a934dc1 | ||
|
|
906c76a959 | ||
|
|
f2467f4411 | ||
|
|
aa68b41971 | ||
|
|
fdabe38180 | ||
|
|
ecdd973c3f | ||
|
|
fe641e9e7c | ||
|
|
0c3a87868c | ||
|
|
278e415e08 | ||
|
|
16882207bb |
No files matched your search
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE market_occurrence_day
|
||||
ADD COLUMN inherit_from_first_day BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,28 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE sales_order
|
||||
ADD COLUMN market_occurrence_id BIGINT,
|
||||
ADD COLUMN market_occurrence_day_id BIGINT;
|
||||
|
||||
ALTER TABLE market_occurrence_day
|
||||
ADD CONSTRAINT uq_market_occurrence_day_occurrence_id UNIQUE (occurrence_id, id);
|
||||
|
||||
ALTER TABLE sales_order
|
||||
ADD CONSTRAINT fk_sales_order_market_occurrence
|
||||
FOREIGN KEY (market_occurrence_id)
|
||||
REFERENCES market_occurrence (id)
|
||||
ON DELETE RESTRICT,
|
||||
ADD CONSTRAINT fk_sales_order_market_occurrence_day
|
||||
FOREIGN KEY (market_occurrence_id, market_occurrence_day_id)
|
||||
REFERENCES market_occurrence_day (occurrence_id, id)
|
||||
ON DELETE RESTRICT,
|
||||
ADD CONSTRAINT chk_sales_order_market_link_complete
|
||||
CHECK ((market_occurrence_id IS NULL) = (market_occurrence_day_id IS NULL));
|
||||
|
||||
CREATE INDEX idx_sales_order_market_occurrence
|
||||
ON sales_order (market_occurrence_id);
|
||||
|
||||
CREATE INDEX idx_sales_order_market_occurrence_day
|
||||
ON sales_order (market_occurrence_day_id);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,55 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE market_organizer (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT,
|
||||
phone TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_market_organizer_status CHECK (status IN ('active', 'inactive'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_market_organizer_name ON market_organizer (LOWER(TRIM(name)));
|
||||
|
||||
INSERT INTO market_organizer (name)
|
||||
VALUES ('Beispielveranstalter 1');
|
||||
|
||||
ALTER TABLE market_definition
|
||||
ADD COLUMN organizer_id BIGINT;
|
||||
|
||||
UPDATE market_definition
|
||||
SET organizer_id = (SELECT id FROM market_organizer WHERE name = 'Beispielveranstalter 1');
|
||||
|
||||
ALTER TABLE market_definition
|
||||
ALTER COLUMN organizer_id SET NOT NULL,
|
||||
ADD CONSTRAINT fk_market_definition_organizer
|
||||
FOREIGN KEY (organizer_id) REFERENCES market_organizer(id) ON DELETE RESTRICT;
|
||||
|
||||
CREATE INDEX idx_market_definition_organizer ON market_definition (organizer_id);
|
||||
|
||||
ALTER TABLE market_occurrence_organizer
|
||||
ADD COLUMN contact_person_name TEXT,
|
||||
ADD COLUMN contact_email TEXT,
|
||||
ADD COLUMN contact_phone TEXT;
|
||||
|
||||
UPDATE market_occurrence_organizer oo
|
||||
SET contact_person_name = NULLIF(TRIM(CONCAT_WS(' ', c.first_name, c.last_name)), ''),
|
||||
contact_email = c.email,
|
||||
contact_phone = c.phone
|
||||
FROM contact c
|
||||
WHERE c.id = oo.contact_person_id;
|
||||
|
||||
ALTER TABLE market_occurrence_organizer
|
||||
DROP COLUMN organization_contact_id,
|
||||
DROP COLUMN contact_person_id;
|
||||
|
||||
INSERT INTO market_occurrence_organizer (occurrence_id, contact_person_name, created_at, updated_at)
|
||||
SELECT o.id, NULL, NOW(), NOW()
|
||||
FROM market_occurrence o
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM market_occurrence_organizer oo WHERE oo.occurrence_id = o.id
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,40 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE market_municipality (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
municipality_reference_id BIGINT UNIQUE REFERENCES market_municipality_reference(id) ON DELETE RESTRICT,
|
||||
name TEXT NOT NULL,
|
||||
canton_code TEXT,
|
||||
bfs_municipality_number INTEGER,
|
||||
data_version DATE,
|
||||
source TEXT NOT NULL DEFAULT 'local',
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_market_municipality_source CHECK (source IN ('reference', 'local'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_market_municipality_name
|
||||
ON market_municipality (LOWER(TRIM(name)));
|
||||
CREATE INDEX idx_market_municipality_name
|
||||
ON market_municipality (name);
|
||||
|
||||
INSERT INTO market_municipality (
|
||||
municipality_reference_id, name, canton_code, bfs_municipality_number, data_version, source, is_active
|
||||
)
|
||||
SELECT id, name, canton_code, bfs_municipality_number, data_version, 'reference', is_active
|
||||
FROM market_municipality_reference;
|
||||
|
||||
ALTER TABLE market_occurrence
|
||||
DROP CONSTRAINT market_occurrence_municipality_reference_id_fkey;
|
||||
|
||||
UPDATE market_occurrence o
|
||||
SET municipality_reference_id = m.id
|
||||
FROM market_municipality m
|
||||
WHERE m.municipality_reference_id = o.municipality_reference_id;
|
||||
|
||||
ALTER TABLE market_occurrence
|
||||
ADD CONSTRAINT fk_market_occurrence_municipality
|
||||
FOREIGN KEY (municipality_reference_id) REFERENCES market_municipality(id) ON DELETE RESTRICT;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,63 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE market_cost_type (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
is_system BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
INSERT INTO market_cost_type (code, name, is_system)
|
||||
VALUES ('stand_fee', 'Standkosten', TRUE);
|
||||
|
||||
ALTER TABLE market_occurrence_cost_item
|
||||
ADD COLUMN cost_type_id BIGINT REFERENCES market_cost_type(id) ON DELETE RESTRICT;
|
||||
|
||||
INSERT INTO market_cost_type (code, name)
|
||||
SELECT DISTINCT
|
||||
'legacy_' || md5(trim(cost_type)),
|
||||
trim(cost_type)
|
||||
FROM market_occurrence_cost_item
|
||||
WHERE trim(cost_type) <> ''
|
||||
AND lower(trim(cost_type)) <> 'standkosten'
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
UPDATE market_occurrence_cost_item item
|
||||
SET cost_type_id = type.id
|
||||
FROM market_cost_type type
|
||||
WHERE type.name = CASE
|
||||
WHEN lower(trim(item.cost_type)) = 'standkosten' THEN 'Standkosten'
|
||||
ELSE trim(item.cost_type)
|
||||
END;
|
||||
|
||||
UPDATE market_occurrence_cost_item item
|
||||
SET cost_type_id = type.id
|
||||
FROM market_cost_type type
|
||||
WHERE type.code = 'stand_fee'
|
||||
AND item.id = (
|
||||
SELECT MIN(first_item.id)
|
||||
FROM market_occurrence_cost_item first_item
|
||||
WHERE first_item.occurrence_id = item.occurrence_id
|
||||
);
|
||||
|
||||
INSERT INTO market_occurrence_cost_item (occurrence_id, cost_type_id, description, gross_amount, tax_status, tax_rate)
|
||||
SELECT occurrence.id, type.id, NULL, 0.00, 'taxable', 8.10
|
||||
FROM market_occurrence occurrence
|
||||
JOIN market_cost_type type ON type.code = 'stand_fee'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM market_occurrence_cost_item item WHERE item.occurrence_id = occurrence.id
|
||||
);
|
||||
|
||||
ALTER TABLE market_occurrence_cost_item
|
||||
ALTER COLUMN cost_type_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE market_occurrence_cost_item
|
||||
DROP COLUMN cost_type;
|
||||
|
||||
CREATE INDEX idx_market_occurrence_cost_item_type
|
||||
ON market_occurrence_cost_item(cost_type_id);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,11 @@
|
||||
BEGIN;
|
||||
|
||||
UPDATE market_occurrence_cost_item item
|
||||
SET tax_status = 'taxable', tax_rate = 8.10, updated_at = NOW()
|
||||
WHERE item.id = (
|
||||
SELECT MIN(first_item.id)
|
||||
FROM market_occurrence_cost_item first_item
|
||||
WHERE first_item.occurrence_id = item.occurrence_id
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@@ -76,7 +76,7 @@ Logistik, Kosten, Zahlung, Vertragslink und Notizen.
|
||||
|
||||
- Wochentag aus dem Datum
|
||||
- Saison aus dem Durchführungsdatum
|
||||
- Gesamtkosten aus den Kostenpositionen
|
||||
- Gesamtkosten aus dem erfassten Bruttobetrag
|
||||
- offener Betrag aus Kosten und bezahltem Betrag
|
||||
- Gesamterlös, Durchschnittswerte und Gesamtzahl verkaufter Artikel aus den
|
||||
Durchführungstagen
|
||||
|
||||
@@ -14,6 +14,7 @@ Status: Verbindliches DB-Ownership-Konzept
|
||||
| `market_occurrence_day` | `market_management` | Marktmanagement-Service |
|
||||
| `market_occurrence_cost_item` | `market_management` | Marktmanagement-Service |
|
||||
| `market_municipality_reference` | `market_management` | Referenzdatenprozess |
|
||||
| `market_municipality` | `market_management` | Marktmanagement-Service |
|
||||
| `market_mcp_idempotency` | `market_management` | MCP-Submodul |
|
||||
|
||||
## 2. Fremde Datenreferenzen
|
||||
@@ -44,6 +45,11 @@ Umsatz- und Artikelmengenaggregation.
|
||||
Der Gemeindereferenzbestand verwendet die BFS-Gemeindenummer als stabile
|
||||
fachliche Referenz und speichert den Datenstand zur Nachvollziehbarkeit.
|
||||
|
||||
Das Marktmanagement besitzt zusätzlich das eigene Gemeinderegister
|
||||
`market_municipality`. Dieses Register enthält die offiziellen Gemeinden sowie
|
||||
lokal neu angelegte Gemeinden und ist die einzige Auswahl- und Schreibbasis für
|
||||
Gemeindezuordnungen von Marktdurchführungen.
|
||||
|
||||
Die Kategorie-/Marktartliste gehört zum Marktmanagement und wird durch Fabienne
|
||||
gepflegt. Hermes darf sie nur lesen.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Hermes: Market-Management-MCP-API
|
||||
|
||||
Stand: 2026-09-19
|
||||
Stand: 2026-09-20
|
||||
Status: Aktive technische Schnittstellendokumentation
|
||||
|
||||
## Verbindung
|
||||
@@ -22,6 +22,59 @@ Der Token wird nicht in dieser Dokumentation oder im Repository gespeichert.
|
||||
|
||||
## Verbindungsablauf
|
||||
|
||||
## Dokumentlinks in Marktdurchführungen
|
||||
|
||||
Wenn Hermes Informationen aus einer Datei verarbeitet und diese in einer
|
||||
Marktdurchführung speichert, muss Hermes die Quelldatei als Link im Feld
|
||||
`notes` aufnehmen. Der bestehende Text darf erhalten bleiben; die Position im
|
||||
Freitext ist frei wählbar.
|
||||
|
||||
Der Link verwendet dieses Format:
|
||||
|
||||
```text
|
||||
<a href="https://erpnaurua.imhochrain.ch/files.php?path=<RFC-3986-url-encodierter-relativer-Pfad>">Dokumentname</a>
|
||||
```
|
||||
|
||||
Der Parameter `path` ist relativ zum Ordner `Hermes Naurua` und enthält nicht
|
||||
den lokalen Mount-Prefix und nicht den Ordnernamen `Hermes Naurua` selbst.
|
||||
Beispiel:
|
||||
|
||||
```text
|
||||
Quelldatei: Hermes Naurua/Marktmanagement/2026 Bellevue Noël/Standmietvertrag_Naurua.pdf
|
||||
Link: <a href="https://erpnaurua.imhochrain.ch/files.php?path=Marktmanagement%2F2026%20Bellevue%20No%C3%ABl%2FStandmietvertrag_Naurua.pdf">Standmietvertrag_Naurua.pdf</a>
|
||||
```
|
||||
|
||||
Leerzeichen, Umlaute und sonstige Sonderzeichen im URL-Parameter müssen
|
||||
vollständig URL-encodiert werden. Im Feld `notes` sind ausschließlich `<a>`
|
||||
für solche internen Dokumentlinks sowie `<br>` und `<strong>` erlaubt. Andere
|
||||
HTML-Tags und nicht erlaubte URL-Schemata werden entfernt. Externe `http://`-
|
||||
und `https://`-Links sind erlaubt und öffnen in einem neuen Tab. Der Link ist
|
||||
schreibgeschützt und erfordert bei internen Dokumenten eine aktive ERP-Anmeldung.
|
||||
Hermes muss diesen HTML-String unverändert als Feldwert senden: keine Markdown-
|
||||
Notation, keine Backticks und keine HTML-Entity-Kodierung wie `<`, `>` oder
|
||||
`"`. Der MCP-Transport erwartet den HTML-Inhalt im JSON-String und escaped
|
||||
ihn nicht als HTML.
|
||||
|
||||
Die Route benötigt auf dem ERP-Webserver die Konfiguration
|
||||
`HERMES_DOCUMENT_ROOT=/volume1/Hermes/Hermes Naurua`. Dieser lokale Pfad ist
|
||||
die auf DEV verifizierte Ablage der Synology-Freigabe.
|
||||
|
||||
## Fachliche Begrifflichkeit für Hermes
|
||||
|
||||
Die fachliche Hierarchie lautet:
|
||||
|
||||
```text
|
||||
Markt (market_definition)
|
||||
└── Marktdurchführung (market_occurrence)
|
||||
└── Markttag / Markttage (market_occurrence_day)
|
||||
```
|
||||
|
||||
Ein Markt ist die zentrale Marktdefinition. Eine Marktdurchführung ist eine
|
||||
konkrete Ausgabe bzw. Teilnahme an diesem Markt. Eine Marktdurchführung besitzt
|
||||
mindestens einen Markttag. OTC-Verkäufe werden immer einem konkreten Markttag
|
||||
zugeordnet; die zugehörige Marktdurchführung und der Markt ergeben sich aus
|
||||
dieser Referenz.
|
||||
|
||||
### 1. Initialisieren
|
||||
|
||||
```json
|
||||
@@ -72,15 +125,22 @@ Das Ergebnis steht bei `tools/call` als JSON im Textinhalt des ersten
|
||||
`municipality_reference_id`, `date_from`, `date_to`, `include_archived`,
|
||||
`sort`, `sort_direction`, `limit`, `offset`
|
||||
- `market_management.search_occurrences`
|
||||
- sucht Marktdurchführungen
|
||||
- optional: `search`, `market_definition_id`, `status`, `place`, `address`,
|
||||
`municipality_reference_id`, `date_from`, `date_to`, `include_archived`,
|
||||
`sort`, `sort_direction`, `limit`, `offset`
|
||||
- `market_management.get_market`
|
||||
- erforderlich: `market_id`
|
||||
- optional: `include_archived`
|
||||
- liefert Marktdurchführungen inklusive Markttagen, Kosten und berechneten Verkaufswerten
|
||||
- `market_management.create_market`
|
||||
- erforderlich: `name`, `category_id`
|
||||
- optional: `website_url` als HTTP(S)-URL der Veranstaltung
|
||||
- erforderlich: `name`, `category_id`, `organizer_id`
|
||||
- `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`
|
||||
@@ -97,20 +157,28 @@ Hermes darf Kategorien lesen und bei einem Markt referenzieren, aber keine
|
||||
Kategorien anlegen oder ändern.
|
||||
|
||||
- `market_management.list_reference_data`
|
||||
- liefert Kategorien, Kantone und aktive Gemeinden
|
||||
- liefert Kategorien, Kantone und aktive Gemeinden aus dem Markt-Gemeinderegister
|
||||
- `market_management.create_municipality`
|
||||
- erforderlich: `name`
|
||||
- legt eine lokale Gemeinde im Markt-Gemeinderegister an
|
||||
- `market_management.get_overview`
|
||||
- liefert serverseitig gefilterte Markt- und Durchführungslisten
|
||||
- liefert serverseitig gefilterte Markt- und Marktdurchführungslisten
|
||||
|
||||
### Durchführungen
|
||||
### Marktdurchführungen
|
||||
|
||||
- `market_management.create_occurrence`
|
||||
- erforderlich: `market_definition_id`, `days` mit mindestens einem Eintrag
|
||||
- erforderlich: `market_definition_id`, `days` mit mindestens einem Markttag
|
||||
- initialer Status: `open`
|
||||
- weitere Durchführungsfelder sind optional und werden typisiert validiert
|
||||
- weitere Marktdurchführungsfelder sind optional und werden typisiert validiert
|
||||
- `market_management.get_occurrence`
|
||||
- erforderlich: `occurrence_id`
|
||||
- liefert zusätzlich den Markt, Markttage, Kosten und
|
||||
berechnete Tages- und Gesamterlöse
|
||||
- `market_management.update_occurrence`
|
||||
- erforderlich: `occurrence_id`
|
||||
- änderbar: sämtliche editierbaren GUI-Marktdurchführungsfelder einschließlich
|
||||
Gemeinde, Kontaktperson, Kontakt-E-Mail und Kontakt-Telefon
|
||||
- der vom Markt geerbte Veranstalter ist nicht editierbar
|
||||
- `market_management.archive_occurrence`
|
||||
- erforderlich: `occurrence_id`
|
||||
- `market_management.reactivate_occurrence`
|
||||
@@ -135,9 +203,10 @@ unpaid
|
||||
paid
|
||||
```
|
||||
|
||||
### Durchführungstage
|
||||
### Markttage
|
||||
|
||||
- `market_management.create_day`
|
||||
- legt einen Markttag an
|
||||
- erforderlich: `occurrence_id`, `date`
|
||||
- `market_management.update_day`
|
||||
- erforderlich: `day_id`
|
||||
@@ -150,18 +219,22 @@ Weitere mögliche Tagesfelder sind `stand_start_time`, `stand_end_time`,
|
||||
|
||||
### Veranstalter und Kosten
|
||||
|
||||
- `market_management.set_organizer`
|
||||
- erforderlich: `occurrence_id`, `organization_contact_id`,
|
||||
`contact_person_id`
|
||||
- `market_management.delete_organizer`
|
||||
- erforderlich: `occurrence_id`
|
||||
- `market_management.create_cost_item`
|
||||
- erforderlich: `occurrence_id`, `cost_type`, `gross_amount`, `tax_status`
|
||||
- `tax_status`: `tax_free` oder `taxable`
|
||||
- `market_management.update_cost_item`
|
||||
- erforderlich: `cost_item_id`
|
||||
- `market_management.delete_cost_item`
|
||||
- erforderlich: `cost_item_id`
|
||||
- Veranstalter werden am Markt über `organizer_id` gesetzt und von den
|
||||
Durchführungen geerbt. Marktdurchführungen können nur `contact_person_name`,
|
||||
`contact_email` und `contact_phone` ändern.
|
||||
- Kosten werden bei `market_management.create_occurrence` automatisch mit einer
|
||||
`Standkosten`-Position (`stand_fee`) und dem Standardsteuersatz 8.1 % angelegt.
|
||||
- `market_management.update_occurrence` ändert Kosten über `gross_cost`,
|
||||
`tax_status`, `tax_rate` sowie die optionale mehrzeilige Freitext-Aufschlüsselung
|
||||
`cost_items_text`.
|
||||
- `cost_items_text` wird beim Schreiben als Beschreibung der automatisch
|
||||
verwalteten `stand_fee`-Kostenposition gespeichert. In den MCP-Leseantworten
|
||||
wird derselbe Wert zusätzlich als `cost_items_text` auf der Durchführung
|
||||
ausgegeben; damit entspricht er direkt dem GUI-Feld „Kostenpositionen“.
|
||||
- `tax_status` ist `taxable` oder `tax_free`; bei `tax_free` ist der Steuersatz
|
||||
`0.00`.
|
||||
- `gross_cost` darf `0.00` sein; einzelne Kostenpositionen und Kostenarten werden
|
||||
nicht separat über MCP verwaltet.
|
||||
|
||||
## Beispiel: Markt anlegen
|
||||
|
||||
@@ -181,7 +254,7 @@ Weitere mögliche Tagesfelder sind `stand_start_time`, `stand_end_time`,
|
||||
}
|
||||
```
|
||||
|
||||
## Beispiel: Durchführung mit Tag anlegen
|
||||
## Beispiel: Marktdurchführung mit Markttag anlegen
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -231,12 +304,12 @@ 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.
|
||||
- Markt und Durchführung werden unabhängig archiviert.
|
||||
- Markt und Marktdurchführung werden unabhängig archiviert.
|
||||
- Alle Schreibvorgänge werden serverseitig validiert.
|
||||
- Jeder schreibende MCP-Aufruf benötigt `idempotency_key`; 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 Durchführungstag bereitstellt.
|
||||
separate OTC-Schnittstelle die Zuordnung zum Markttag bereitstellt.
|
||||
|
||||
## Fehler
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ Vorgesehene Toolgruppen:
|
||||
|
||||
- Märkte suchen und lesen
|
||||
- Märkte anlegen, ändern, archivieren und reaktivieren
|
||||
- Durchführungen suchen und lesen
|
||||
- Durchführungen anlegen, ändern, archivieren und reaktivieren
|
||||
- Durchführungstage anlegen, ändern und entfernen
|
||||
- Marktdurchführungen suchen und lesen
|
||||
- Marktdurchführungen anlegen, ändern, archivieren und reaktivieren
|
||||
- Markttage anlegen, ändern und entfernen
|
||||
- Kategorien, Kantone und Gemeinden lesen
|
||||
- Kosten und Zahlungsstatus ändern
|
||||
- Übersichtsdaten lesen
|
||||
@@ -68,7 +68,15 @@ Modulberechtigung.
|
||||
- Fehlgeschlagene Aufrufe erzeugen keine Teilpersistierung.
|
||||
- Alle Schreibvorgänge laufen über die interne Modul-API.
|
||||
- Hermes darf bei einem neuen Markt nur eine bestehende Kategorie referenzieren.
|
||||
- Dokumentlinks können optional als Herkunftsinformation mitgegeben werden.
|
||||
- Wenn Hermes Informationen aus einer Datei verarbeitet und in einer
|
||||
Marktdurchführung speichert, muss die Quelldatei als interner `<a>`-Link im
|
||||
Feld `notes` referenziert werden. Zusätzlich sind in mehrzeiligen Feldern
|
||||
`<br>` und `<strong>` erlaubt. Die Position im bestehenden Freitext ist frei
|
||||
wählbar. Externe `http://`- und `https://`-Links sind zulässig und öffnen in
|
||||
einem neuen Tab; andere HTML-Tags und URL-Schemata sind nicht zulässig. Hermes
|
||||
sendet den HTML-String unverändert; Markdown, Backticks und HTML-Entities wie
|
||||
`<`, `>` oder `"` werden nicht verwendet. Der MCP-Transport escaped
|
||||
den HTML-Inhalt nicht.
|
||||
- Unsichere Erkennung wird nicht durch eine zusätzliche App-Freigabelogik
|
||||
behandelt; die fachliche Qualitätssicherung liegt im Hermes-Training.
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ Tabellen:
|
||||
- `market_occurrence_day`
|
||||
- `market_occurrence_cost_item`
|
||||
- `market_municipality_reference`
|
||||
- `market_municipality`
|
||||
- `market_mcp_idempotency`
|
||||
|
||||
Bestehende Integrationsreferenz:
|
||||
@@ -84,6 +85,7 @@ Normativ:
|
||||
|
||||
- jeder Markt besitzt genau einen eindeutigen Namen
|
||||
- jeder Markt besitzt genau eine Kategorie
|
||||
- jeder Markt besitzt genau einen verpflichtenden Veranstalter aus dem eigenen Markt-Veranstalterregister
|
||||
- jeder Markt kann eine optionale Website-URL der Veranstaltung besitzen
|
||||
- der Markt enthält keine Durchführungs-, Kontakt-, Kosten-, Zahlungs- oder
|
||||
Erlösdaten
|
||||
@@ -101,6 +103,7 @@ Normativ:
|
||||
Zahlungsdaten gehören zur Durchführung
|
||||
- der Status gehört zur Durchführung
|
||||
- Zahlungsstatus und Marktstatus sind unabhängig
|
||||
- der Veranstalter wird von jeder Durchführung des Marktes geerbt und ist dort nicht editierbar
|
||||
- der Zahlungsstatus ist manuell `paid` oder `unpaid`
|
||||
|
||||
Minimale Pflichtfelder über alle Statuswerte hinweg:
|
||||
@@ -115,10 +118,10 @@ Veranstalterbeziehung einer Durchführung.
|
||||
|
||||
Normativ:
|
||||
|
||||
- der Veranstalter ist eine Organisation aus dem Kontaktmodul
|
||||
- jede Durchführung besitzt höchstens genau einen Veranstalterdatensatz
|
||||
- der Veranstalterdatensatz referenziert genau eine Kontaktperson
|
||||
- Kontakt- und Organisationsdaten werden nicht im Marktmanagement dupliziert
|
||||
- die Tabelle enthält die durchführungsbezogene Kontaktperson als Freitext sowie optionale E-Mail und Telefonnummer
|
||||
- jede Durchführung besitzt genau einen lokalen Kontaktdatensatz
|
||||
- der Veranstalter selbst wird ausschließlich über `market_definition.organizer_id` vom Markt geerbt
|
||||
- Veranstalter werden ausschließlich im Markt-Veranstalterregister verwaltet und nicht aus Lieferanten oder Kunden abgeleitet
|
||||
|
||||
### 3.5 `market_occurrence_day`
|
||||
|
||||
@@ -151,19 +154,21 @@ Minimale Pflichtfelder:
|
||||
Alle weiteren Felder bleiben bei der Anlage optional und werden unabhängig vom
|
||||
Status ergänzt.
|
||||
|
||||
### 3.6 `market_occurrence_cost_item`
|
||||
### 3.6 Kosten einer Durchführung
|
||||
|
||||
Einzelne Kostenposition einer Durchführung.
|
||||
Die Durchführung besitzt einen einfachen Gesamtkostenbetrag und eine optionale
|
||||
mehrzeilige Freitext-Aufschlüsselung der Kostenpositionen.
|
||||
|
||||
Normativ:
|
||||
|
||||
- jede Kostenposition gehört genau einer Durchführung
|
||||
- jede Durchführung besitzt genau eine technische `Standkosten`-Position
|
||||
(`stand_fee`); deren Betrag darf `0.00` sein und der Standardsteuersatz beträgt
|
||||
8.1 %
|
||||
- weitere Einzelpositionen und Kostenarten werden nicht separat gepflegt
|
||||
- CHF ist die einzige Währung
|
||||
- Bruttobetrag wird erfasst
|
||||
- steuerfreie Positionen sind zulässig
|
||||
- Netto- und Steuerbetrag werden aus Brutto und Steuerart/-satz berechnet
|
||||
- Gesamtkosten sind die Summe der Kostenpositionen und nicht manuell
|
||||
überschreibbar
|
||||
- die Freitext-Aufschlüsselung wird als Beschreibung gespeichert
|
||||
|
||||
### 3.7 `market_municipality_reference`
|
||||
|
||||
@@ -174,9 +179,24 @@ Normativ:
|
||||
- Gemeinden werden über BFS-Gemeindenummern referenziert
|
||||
- Gemeindename und Kantonszuordnung stammen aus dem Referenzbestand
|
||||
- nur gültige Gemeinden stehen für neue Eingaben zur Auswahl
|
||||
|
||||
### 3.8 `market_municipality`
|
||||
|
||||
Eigenes Gemeinderegister des Marktmanagements. Es enthält sowohl aktive
|
||||
Gemeinden aus dem offiziellen Referenzbestand als auch lokal neu angelegte
|
||||
Gemeinden.
|
||||
|
||||
Normativ:
|
||||
|
||||
- jede aktive Gemeinde besitzt einen eindeutigen Namen
|
||||
- Referenzgemeinden behalten ihre BFS-Gemeindenummer und den Datenstand
|
||||
- lokal angelegte Gemeinden besitzen keine BFS-Referenz
|
||||
- neue Gemeinden werden über das Marktmanagement angelegt und danach in der
|
||||
Auswahl für weitere Marktdurchführungen angeboten
|
||||
- eine Marktdurchführung referenziert genau einen Datensatz dieses Registers
|
||||
- der Datenstand wird versioniert gespeichert
|
||||
|
||||
### 3.8 `market_mcp_idempotency`
|
||||
### 3.9 `market_mcp_idempotency`
|
||||
|
||||
Technischer Idempotenzbestand für wiederholte Hermes-Schreibaufrufe.
|
||||
|
||||
@@ -253,16 +273,16 @@ Schreibt:
|
||||
|
||||
Fachliche Aufgabe:
|
||||
|
||||
- Kostenpositionen und manuelle Zahlungsinformationen pflegen
|
||||
- Gesamtkosten, Kostenaufschlüsselung und manuelle Zahlungsinformationen pflegen
|
||||
|
||||
Liest:
|
||||
|
||||
- Durchführung
|
||||
- bestehende Kostenpositionen
|
||||
- bestehende Kosteninformation
|
||||
|
||||
Schreibt:
|
||||
|
||||
- `market_occurrence_cost_item`
|
||||
- technische Standkosten-Position in `market_occurrence_cost_item`
|
||||
- Zahlungsfelder in `market_occurrence`
|
||||
|
||||
### 4.5 OTC-Verkauf einem Durchführungstag zuordnen
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
# market_management.occurrence_status_completion
|
||||
Stand: 2026-09-19
|
||||
Status: Verbindliche Prozess-Spezifikation
|
||||
|
||||
## 1. Zweck
|
||||
|
||||
Setzt aktive Durchführungen am täglichen Cron-Lauf automatisch auf
|
||||
`completed`, sobald der Kalendertag nach ihrem letzten Durchführungstag erreicht
|
||||
ist. Abgesagte Durchführungen werden nicht verändert.
|
||||
|
||||
## 2. Prozess-Einbettung
|
||||
|
||||
Täglicher synchroner Cron-Teilprozess des zentralen System-Cron-Entrypoints.
|
||||
Die fachliche Statusänderung gehört zum owning Modul `market_management`.
|
||||
|
||||
## 3. Input
|
||||
|
||||
- aktuelles Datum in der Zeitzone `Europe/Zurich`
|
||||
- keine vorbereiteten Fachdaten; der Prozess liest seine Daten selbst aus der
|
||||
Datenbank
|
||||
|
||||
## 4. Exakter Prozessablauf
|
||||
|
||||
1. Einen technischen Lauf in `public.process_runs` mit dem aktuellen Datum
|
||||
starten.
|
||||
2. Aus `market_occurrence` und `market_occurrence_day` aktive Durchführungen
|
||||
mit mindestens einem Tag bestimmen.
|
||||
3. Durchführungen ausschliessen, deren Status `cancelled` oder bereits
|
||||
`completed` ist.
|
||||
4. Für jede verbleibende Durchführung den letzten Durchführungstag bestimmen.
|
||||
5. Wenn dieser Tag vor dem aktuellen Datum liegt, den Status atomar auf
|
||||
`completed` setzen.
|
||||
6. Den technischen Lauf mit Anzahl und IDs der geänderten Durchführungen
|
||||
abschliessen.
|
||||
|
||||
Gelesen werden `market_occurrence` und `market_occurrence_day`. Geschrieben
|
||||
werden `market_occurrence.status`, `market_occurrence.updated_at` und der
|
||||
technische Lauf in `public.process_runs`.
|
||||
|
||||
## 5. Batch, Betriebsmodell und Einbettung
|
||||
|
||||
Ein täglicher synchroner Cron-Lauf; ein SQL-Update verarbeitet den vollständigen
|
||||
aktuellen Scope atomar. Die bestehende Cron-Sperre verhindert parallele Läufe.
|
||||
|
||||
## 6. Output
|
||||
|
||||
Ein kompakter Laufstatus mit aktuellem Datum, Anzahl und IDs der auf
|
||||
`completed` gesetzten Durchführungen sowie der technischen Lauf-ID.
|
||||
|
||||
## 7. Erfolgskriterien
|
||||
|
||||
- Eine Durchführung wird am Tag nach ihrem letzten Durchführungstag auf
|
||||
`completed` gesetzt.
|
||||
- Abgesagte und bereits abgeschlossene Durchführungen bleiben unverändert.
|
||||
- Durchführungen ohne Durchführungstag werden nicht verändert.
|
||||
- Wiederholte Cron-Läufe sind idempotent.
|
||||
- `public.process_runs` enthält einen abgeschlossenen technischen Lauf.
|
||||
|
||||
## 8. Fehlerschranke
|
||||
|
||||
Ein technischer Fehler bricht den Lauf ab, rollt die fachliche Transaktion
|
||||
zurück und markiert `process_runs` als `failed`.
|
||||
|
||||
## 9. Fachliche Betriebsregel
|
||||
|
||||
Die Prüfung erfolgt einmal täglich über den bestehenden Cron. Die Statusänderung
|
||||
ist unabhängig von GUI-Aufrufen und OTC-Bestellungen.
|
||||
|
||||
## 10. Sub-Prozess-Referenzen
|
||||
|
||||
Keine zusätzlichen Sub-Prozesse.
|
||||
|
||||
## 11. End-to-End Sub-Prozess-Kette
|
||||
|
||||
System-Cron → Statusabschlussprozess → `market_occurrence` und
|
||||
`public.process_runs`.
|
||||
|
||||
## 12. Sub-Prozess-Wiederverwendung
|
||||
|
||||
Der bestehende System-Cron-Entrypoint und die bestehende Cron-Sperre werden
|
||||
wiederverwendet. Ein zusätzlicher Scheduler wird nicht eingeführt.
|
||||
|
||||
## 13. Step-Liste
|
||||
|
||||
- Lauf starten
|
||||
- eligible Durchführungen bestimmen und abschliessen
|
||||
- Laufstatus persistieren
|
||||
|
||||
## 14. Parallelisierung
|
||||
|
||||
Keine Parallelisierung; der deterministische SQL-Batch läuft synchron und
|
||||
atomar.
|
||||
|
||||
## 15. Output-Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "done",
|
||||
"as_of_date": "YYYY-MM-DD",
|
||||
"completed_count": 0,
|
||||
"completed_occurrence_ids": [],
|
||||
"technical_run_id": 0
|
||||
}
|
||||
```
|
||||
|
||||
## 16. Done-Kriterien
|
||||
|
||||
- Der tägliche Cron ruft den Prozess genau einmal auf.
|
||||
- Die Statusregel ist serverseitig implementiert.
|
||||
- Der Prozess schreibt `process_runs` vollständig.
|
||||
- Wiederholte Läufe erzeugen keine erneuten Statusänderungen.
|
||||
@@ -159,7 +159,7 @@ Abgeleitete Werte werden nicht manuell eingegeben:
|
||||
|
||||
- Wochentag aus dem Datum
|
||||
- Saison aus dem Durchführungsdatum
|
||||
- Gesamtkosten aus den Kostenpositionen
|
||||
- Gesamtkosten als manuell erfassbarer Bruttobetrag mit optionaler Freitext-Aufschlüsselung
|
||||
- offener Betrag aus Kosten und bezahltem Betrag
|
||||
- Gesamterlös, Durchschnittswerte und Gesamtzahl verkaufter Artikel aus den
|
||||
Durchführungstagen
|
||||
|
||||
@@ -8,6 +8,7 @@ Titelaktionen, Lesemodus und optionalem Bearbeitungsmodus.
|
||||
Das Pattern kapselt nur die technische Interaktion:
|
||||
|
||||
- Umschalten zwischen Lese- und Bearbeitungsansicht
|
||||
- Sichtbarer, im Bearbeitungsmodus deaktivierter Bearbeiten-Button
|
||||
- Abbrechen einer Bearbeitung
|
||||
- optionale Loeschbestaetigung mit konfigurierbarem Token
|
||||
- Deaktivieren der Loeschaktion bis zur gueltigen Bestaetigung
|
||||
@@ -31,6 +32,10 @@ Bedienelemente verwenden folgende Attribute:
|
||||
- optional `data-shared-editable-card-delete-submit`
|
||||
- optional `data-shared-editable-card-delete-cancel`
|
||||
|
||||
Der Bearbeiten-Button bleibt im Titelsegment sichtbar. Im Bearbeitungsmodus
|
||||
wird er deaktiviert (`disabled`, `aria-disabled="true"` und
|
||||
`sg-button--inactive`); die Aktionen zum Abbrechen und Speichern bleiben aktiv.
|
||||
|
||||
Das Loesch-Token wird je Karte mit
|
||||
`data-shared-editable-delete-token` konfiguriert. Die Standardaktion fuer das
|
||||
Loeschen bleibt im owning Modul und verwendet den bestehenden fachlichen
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Mobile Detail
|
||||
|
||||
## Zweck
|
||||
|
||||
`SharedMobileDetail` ist ein fachlich neutrales Frontend-Pattern fuer mobile
|
||||
Listen-/Detailansichten. Es ersetzt auf kleinen Viewports den sichtbaren
|
||||
Drawer durch eine vollflaechige Detailansicht.
|
||||
|
||||
## Verantwortung
|
||||
|
||||
Das Pattern kapselt ausschliesslich technische Interaktion:
|
||||
|
||||
- Verschieben des vorhandenen Detail-DOMs in einen mobilen Host
|
||||
- Anzeigen und Entfernen des mobilen Hosts
|
||||
- Browser-History und `popstate`-kompatibler Zurueck-Navigation
|
||||
- Zuruecksetzen des Body-Locks und des globalen Drawer-Zustands
|
||||
- Zuruecksetzen der Drawer-Attribute beim Verlassen der Detailansicht
|
||||
|
||||
Fachliche Darstellung, API-Aufrufe, Validierung und Speichern bleiben im
|
||||
owning Modul.
|
||||
|
||||
## Nutzung
|
||||
|
||||
Fachliche Bereiche erzeugen einen Controller mit eigenem Host-/Drawer-
|
||||
Attribut, eigener Body-Klasse und einem bestehenden Styleguide-
|
||||
Navigationskarten-Markup fuer die Zurueck-Navigation. Der Controller darf
|
||||
keine fachlichen Daten oder fachlichen Zustandsentscheidungen enthalten.
|
||||
@@ -1 +1 @@
|
||||
2026.05.18.1
|
||||
2026.05.18.2
|
||||
@@ -1,41 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Styleguide – Komponente E-Mail Footer</title>
|
||||
<link rel="stylesheet" href="../styleguide.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1 class="sg-main-heading">Komponente – E-Mail Footer</h1>
|
||||
|
||||
<section id="component-email-footer">
|
||||
<p class="sg-preview-label">Komponente: E-Mail Footer</p>
|
||||
|
||||
<p class="sg-preview-label">E-Mail Footer not subscribed</p>
|
||||
|
||||
<div class="sg-preview-area">
|
||||
<article
|
||||
class="sg-card sg-card--content-card sg-card--footer-card"
|
||||
data-component="content-card"
|
||||
data-component-context="email-footer"
|
||||
data-component-variant="not-subscribed"
|
||||
aria-label="E-Mail Footer not subscribed"
|
||||
>
|
||||
<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body">
|
||||
<div class="sg-text-layout-pattern__sample sg-text-layout-pattern__two-column" data-pattern-part="text-block-two-column">
|
||||
<p class="sg-body sg-text-layout-pattern__column">
|
||||
Footer links: Platzhaltertext fuer allgemeine Hinweise, Navigation oder Kontaktinformationen im zweispaltigen Layout.
|
||||
</p>
|
||||
<p class="sg-body sg-text-layout-pattern__column">
|
||||
Footer rechts: Platzhaltertext fuer ergaenzende Angaben, rechtliche Hinweise oder sekundäre Footer-Inhalte im gleichen Raster.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,37 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Styleguide – Komponente E-Mail Header</title>
|
||||
<link rel="stylesheet" href="../styleguide.css">
|
||||
<link rel="stylesheet" href="../email-styleguide.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1 class="sg-main-heading">Komponente – E-Mail Header</h1>
|
||||
|
||||
<section id="component-email-header">
|
||||
<p class="sg-preview-label">Komponente: E-Mail Header</p>
|
||||
|
||||
<div class="sg-preview-area">
|
||||
<div class="sg-email-preview-viewport">
|
||||
<div class="sg-email-preview-body">
|
||||
<header
|
||||
class="sg-email-header"
|
||||
aria-label="E-Mail Header"
|
||||
data-component="email-header"
|
||||
data-component-variant="logo-only"
|
||||
data-derived-from="portal-header-without-options-row"
|
||||
>
|
||||
<div class="sg-email-header__main" data-component-part="main">
|
||||
<p class="sg-email-header__brand" data-component-part="brand">ValueStockFinder</p>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -836,6 +836,21 @@
|
||||
</span>
|
||||
</section>
|
||||
|
||||
<!-- Component: Tooltip -->
|
||||
<section id="component-tooltip">
|
||||
<p class="sg-preview-label">Component: Tooltip</p>
|
||||
|
||||
<span class="sg-tooltip-wrap" data-component="tooltip">
|
||||
<button class="sg-interaction-element sg-button sg-button--active" type="button" aria-describedby="component-tooltip-panel" aria-expanded="false" data-component-part="tooltip-trigger">
|
||||
Tooltip anzeigen
|
||||
</button>
|
||||
|
||||
<span class="sg-tooltip-panel sg-table-label" id="component-tooltip-panel" role="tooltip" hidden data-component-part="tooltip-panel">
|
||||
Diese Sprechblase ist trigger-verankert und wechselt bei fehlendem Platz automatisch die Seite.
|
||||
</span>
|
||||
</span>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Component: Hyperlink -->
|
||||
<section id="component-hyperlink">
|
||||
@@ -847,6 +862,7 @@
|
||||
</section>
|
||||
|
||||
<script src="../scripts/help-icon-overlays.js"></script>
|
||||
<script src="../scripts/tooltip-overlays.js"></script>
|
||||
<section id="component-file-input">
|
||||
<p class="sg-preview-label">Component: File Input</p>
|
||||
<div class="sg-form-preview-area">
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<tr><td>white</td><td>#FFFFFF</td><td>Standardfläche von Buttons, Eingabefeldern, Pulldown-Triggern, Checkboxen, Radios, Toggle-Tracks und Card-Trennern.</td></tr>
|
||||
<tr><td>signal-green</td><td>#009101</td><td>Positive Score-Balken und positive Chart-Werte.</td></tr>
|
||||
<tr><td>signal-green-light</td><td>#65A665</td><td>Hellgrüne Variante für positive Signal- und Notification-Flächen.</td></tr>
|
||||
<tr><td>signal-green-red</td><td>#C0624D</td><td>Rotgrüne Variante für positive oder negative Bewertungsdarstellungen in Notifications.</td></tr>
|
||||
<tr><td>signal-yellow</td><td>#9C7A00</td><td>Neutrale Score-Balken, neutrale Chart-Werte und Warnwerte in Tabellen.</td></tr>
|
||||
<tr><td>ampel-hellgelb</td><td>#D3B962</td><td>Untersegmente in der Pattern-Notification-Variante der Object Card.</td></tr>
|
||||
<tr><td>signal-red</td><td>#9B3B2F</td><td>Negative Score-Balken und negative Chart-Werte.</td></tr>
|
||||
@@ -38,13 +39,29 @@
|
||||
<tr><td>font-light</td><td>#FFFFFF</td><td>Schrift auf dunklen Flächen, Portalheader, Card-Headern, Prozessbuttons und ausgewählten Tab-Buttons.</td></tr>
|
||||
<tr><td>font-hyperlink</td><td>#FF6900</td><td>Links in Texten, Prozessbuttons und Page-Navigations-Cards.</td></tr>
|
||||
<tr><td>process-inactive</td><td>#FFAE79</td><td>Inaktive Hintergrundfläche des Prozessbuttons.</td></tr>
|
||||
<tr><td>background-purple</td><td>#373C4A</td><td>Globaler Portal- und E-Mail-Hintergrund.</td></tr>
|
||||
<tr><td>background-purple</td><td>#373C4A</td><td>Globaler Portalhintergrund.</td></tr>
|
||||
<tr><td>background-purple-light</td><td>#656C7D</td><td>Group Cards und gruppierende Card-Flächen.</td></tr>
|
||||
<tr><td>transparent</td><td>transparent</td><td>Transparenter Foundation-Wert für semantische Overlay-/Box-Varianten ohne Eigenfläche.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="foundations-card-text">
|
||||
<h2 class="sg-sub-heading sg-text-on-dark">Card Semantic Tokens</h2>
|
||||
<table class="sg-foundation-table sg-table-label">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Wert</th>
|
||||
<th>Verwendung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>text-card-signal</td><td>font-light</td><td>Weiße Schrift auf allen farbigen Signal-Segmenten, unabhängig vom übergeordneten Card-Kontext.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="foundations-states">
|
||||
<h2 class="sg-sub-heading sg-text-on-dark">States</h2>
|
||||
<table class="sg-foundation-table sg-table-label">
|
||||
@@ -156,7 +173,6 @@
|
||||
<tr><td>input-label-width</td><td>9rem</td><td>Desktop-Breite der Label-Spalte für gruppierte Formularzeilen mit Pulldowns, Slidern, Radio-Feldern, Checkbox-Feldern sowie ein- und mehrzeiligen Eingabefeldern.</td></tr>
|
||||
<tr><td>input-field-desktop-width</td><td>400px</td><td>Fixe Desktop-Breite von ein- und mehrzeiligen Eingabefeldern in der Input-Component-Preview.</td></tr>
|
||||
<tr><td>input-field-max-width</td><td>600px</td><td>Maximale Breite von ein- und mehrzeiligen Eingabefeldern (ohne Suchfeld).</td></tr>
|
||||
<tr><td>email-body-max-width</td><td>600px</td><td>Maximale Breite des Inhaltsbereichs von E-Mail-Templates innerhalb des E-Mail-Viewports.</td></tr>
|
||||
<tr><td>object-card-height</td><td>600px</td><td>Fixe Höhe der Object Card im Desktop-Layout (u. a. für Company Card).</td></tr>
|
||||
<tr><td>scrollable-card-width</td><td>600px</td><td>Standardbreite der eigenständigen Scrollable Card; kann im konsumierenden Portal über den semantischen Layout-Token angepasst werden.</td></tr>
|
||||
<tr><td>scrollable-card-height</td><td>600px</td><td>Standardhöhe der eigenständigen Scrollable Card; kann im konsumierenden Portal über den semantischen Layout-Token angepasst werden.</td></tr>
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<li><a href="./patterns/navigation-card.html">Navigation Card</a></li>
|
||||
<li><a href="./patterns/left-navigation.html">Left Navigation</a></li>
|
||||
<li><a href="./patterns/notifications.html">Notifications</a></li>
|
||||
<li><a href="./patterns/history-graph.html">History Graph</a></li>
|
||||
<li><a href="./patterns/content-cards-group.html">Content Cards Group</a></li>
|
||||
<li><a href="./patterns/card-gruppe-mit-tastennavigation.html">Card Gruppe mit Tastennavigation</a></li>
|
||||
<li><a href="./patterns/formular-mit-abschnitten.html">Formular mit Abschnitten</a></li>
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Styleguide – Pattern History Graph</title>
|
||||
<link rel="stylesheet" href="../styleguide.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1 class="sg-main-heading">Pattern – History Graph</h1>
|
||||
|
||||
<section id="pattern-history-graph">
|
||||
<p class="sg-preview-label">Pattern: History Graph</p>
|
||||
|
||||
<div class="sg-preview-area sg-history-graph" aria-label="Verlauf von Ereignissen">
|
||||
<ol class="sg-history-graph__events">
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Aktuellstes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green" data-component-part="card-header">
|
||||
<p class="sg-body">Aktuellstes Ereignis mit einem einzelnen farbigen Segment.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Zweites Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-yellow" data-component-part="card-header">
|
||||
<p class="sg-body">Ereignis mit mehreren unabhängig gefärbten Segmenten.</p>
|
||||
</div>
|
||||
<div class="sg-card-segment sg-card-segment--body sg-card-segment--white" data-component-part="card-body">
|
||||
<p class="sg-body">Der Inhalt, zum Beispiel Datum und Beschreibung, wird individuell durch das konsumierende Portal definiert.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Ältestes dargestelltes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-red" data-component-part="card-header">
|
||||
<p class="sg-body">Ältestes dargestelltes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Viertes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green-light" data-component-part="card-header">
|
||||
<p class="sg-body">Viertes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Fünftes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-yellow" data-component-part="card-header">
|
||||
<p class="sg-body">Fünftes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Sechstes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green" data-component-part="card-header">
|
||||
<p class="sg-body">Sechstes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Siebtes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-red" data-component-part="card-header">
|
||||
<p class="sg-body">Siebtes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Achtes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green-light" data-component-part="card-header">
|
||||
<p class="sg-body">Achtes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Neuntes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-yellow" data-component-part="card-header">
|
||||
<p class="sg-body">Neuntes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Zehntes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green" data-component-part="card-header">
|
||||
<p class="sg-body">Zehntes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<!-- Wird nur gerendert, wenn weitere Ereignisse geladen werden können. -->
|
||||
<div class="sg-history-graph__navigation">
|
||||
<div class="sg-navigation-card-layout">
|
||||
<div class="sg-navigation-card-block">
|
||||
<article class="sg-card" data-component="card" data-pattern="navigation-card" aria-label="Weitere Ereignisse laden">
|
||||
<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment">
|
||||
<div class="sg-navigation-card-center">
|
||||
<a class="sg-hyperlink" href="#" data-component="hyperlink">Mehr laden</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -142,6 +142,27 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<p class="sg-preview-label">Pattern: Notification with title - green-red rating</p>
|
||||
<article class="sg-card sg-card--notification-signal-green-red sg-notifications-pattern__card" data-pattern="notification-with-title" data-pattern-variant="green-red-rating" data-component="notification-card">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green-red sg-notifications-pattern__text-segment sg-notifications-pattern__title-segment" data-component-part="card-header">
|
||||
<p class="sg-body">
|
||||
<span class="sg-strong">Bewertung</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green-red sg-notifications-pattern__text-segment" data-component-part="card-header">
|
||||
<p class="sg-body">
|
||||
Positive oder negative Bewertungsdarstellung zur Illustration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="sg-card-segment sg-card-segment--body sg-card-segment--signal-green-red" data-component-part="card-body">
|
||||
<button class="sg-interaction-element sg-button sg-button--active" type="button" data-component="button" data-component-state="active">
|
||||
zur Bewertung
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<p class="sg-preview-label">Pattern: Notification with title - yellow</p>
|
||||
<article class="sg-card sg-notifications-pattern__card" data-pattern="notification-with-title" data-pattern-variant="yellow" data-component="notification-card">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-yellow sg-notifications-pattern__text-segment sg-notifications-pattern__title-segment" data-component-part="card-header">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</p>
|
||||
|
||||
<p class="sg-table-label" style="color: var(--text-options-row-description);">
|
||||
Mobile: rechte Optionen werden zum oberen Segment, linke Optionen zum unteren Segment; linke Optionen brechen innerhalb der verfügbaren Breite um.
|
||||
Mobile: Pulldowns stehen mit maximal zwei Elementen pro Zeile; ein Suchfeld steht immer allein in einer 100-%-Zeile.
|
||||
</p>
|
||||
|
||||
<div class="sg-options-row" aria-label="Optionszeile" data-pattern="options-row">
|
||||
@@ -83,6 +83,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sg-pulldown-demo" data-open="false" data-align="left" data-selection-mode="single" data-component="pulldown" data-component-context="options-row" data-component-state="inactive-selectable">
|
||||
<button class="sg-interaction-element sg-pulldown sg-pulldown-demo__trigger" type="button" aria-expanded="false" aria-label="Very long English label for market selection" data-component-part="pulldown-trigger" data-label-base="Very long English market selection label">
|
||||
Very long English market selection label
|
||||
</button>
|
||||
|
||||
<div class="sg-pulldown-panel" aria-label="Opened market selection" data-component-part="pulldown-panel">
|
||||
<ul class="sg-pulldown-option-list" aria-label="Market options">
|
||||
<li class="sg-pulldown-option" role="checkbox" aria-checked="false" data-pulldown-option>
|
||||
<span>United Kingdom</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sg-search-field-row">
|
||||
<span class="sg-input-single-line-wrap sg-search-field-input" data-has-value="false" data-component="single-line-input" data-component-context="options-row" data-component-state="inactive-selectable">
|
||||
<input
|
||||
@@ -120,6 +134,39 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="pattern-options-row-mobile-cases">
|
||||
<p class="sg-preview-label">Pattern: Options Row – Mobile-Fälle</p>
|
||||
<p class="sg-table-label" style="color: var(--text-options-row-description);">
|
||||
Prüffälle: kein Pulldown, ein Pulldown, Suchfeld ohne Ergebniszähler sowie lange deutsche Beschriftung.
|
||||
</p>
|
||||
|
||||
<div class="sg-options-row sg-options-row--left-only" aria-label="Optionszeile ohne Pulldown" data-pattern="options-row-mobile-no-pulldown">
|
||||
<div class="sg-options-row__left" data-pattern-part="options-row-primary-actions">
|
||||
<div class="sg-search-field-row">
|
||||
<span class="sg-input-single-line-wrap sg-search-field-input" data-has-value="false" data-component="single-line-input" data-component-context="options-row" data-component-state="inactive-selectable">
|
||||
<input class="sg-interaction-element sg-input-single-line" type="text" placeholder="Suche ohne Ergebniszähler" aria-label="Suche ohne Ergebniszähler">
|
||||
<button class="sg-input-clear-button" type="button" aria-label="Eingabe löschen">×</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sg-options-row sg-options-row--left-only" aria-label="Optionszeile mit einem Pulldown" data-pattern="options-row-mobile-one-pulldown">
|
||||
<div class="sg-options-row__left" data-pattern-part="options-row-primary-actions">
|
||||
<div class="sg-pulldown-demo" data-open="false" data-align="left" data-selection-mode="single" data-component="pulldown" data-component-context="options-row" data-component-state="inactive-selectable">
|
||||
<button class="sg-interaction-element sg-pulldown sg-pulldown-demo__trigger" type="button" aria-expanded="false" aria-label="Sehr lange deutsche Beschriftung für eine Auswahl" data-component-part="pulldown-trigger" data-label-base="Sehr lange deutsche Beschriftung für eine Auswahl">
|
||||
Sehr lange deutsche Beschriftung für eine Auswahl
|
||||
</button>
|
||||
<div class="sg-pulldown-panel" aria-label="Geöffnete Auswahl" data-component-part="pulldown-panel">
|
||||
<ul class="sg-pulldown-option-list" aria-label="Auswahloptionen">
|
||||
<li class="sg-pulldown-option" role="checkbox" aria-checked="false" data-pulldown-option><span>Option</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="pattern-options-row-left-only">
|
||||
<p class="sg-preview-label">Pattern: Options row left only</p>
|
||||
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
(function initTooltipOverlayModule() {
|
||||
let tooltipId = 0;
|
||||
const states = new WeakMap();
|
||||
const tooltipStates = new Set();
|
||||
|
||||
const getViewport = () => ({
|
||||
width: window.visualViewport?.width || window.innerWidth,
|
||||
height: window.visualViewport?.height || window.innerHeight,
|
||||
});
|
||||
|
||||
const getPixelValue = (propertyName) => {
|
||||
const rootStyles = getComputedStyle(document.documentElement);
|
||||
const rawValue = rootStyles.getPropertyValue(propertyName).trim();
|
||||
const rootFontSize = parseFloat(rootStyles.fontSize) || 16;
|
||||
const value = parseFloat(rawValue);
|
||||
|
||||
if (Number.isNaN(value)) {
|
||||
return 0;
|
||||
}
|
||||
if (rawValue.endsWith('rem')) {
|
||||
return value * rootFontSize;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const getElements = (wrap) => ({
|
||||
trigger: wrap.querySelector('[data-component-part="tooltip-trigger"]'),
|
||||
panel: wrap.querySelector('[data-component-part="tooltip-panel"]'),
|
||||
});
|
||||
|
||||
const ensureAccessibility = (state) => {
|
||||
const { trigger, panel } = state;
|
||||
panel.setAttribute('role', 'tooltip');
|
||||
|
||||
if (!panel.id) {
|
||||
tooltipId += 1;
|
||||
panel.id = `sg-tooltip-panel-${tooltipId}`;
|
||||
}
|
||||
|
||||
const describedBy = (trigger.getAttribute('aria-describedby') || '')
|
||||
.split(/\s+/)
|
||||
.filter(Boolean);
|
||||
if (!describedBy.includes(panel.id)) {
|
||||
trigger.setAttribute('aria-describedby', [...describedBy, panel.id].join(' '));
|
||||
}
|
||||
|
||||
const hasAccessibleName = Boolean(
|
||||
trigger.getAttribute('aria-label')
|
||||
|| trigger.getAttribute('aria-labelledby')
|
||||
|| trigger.getAttribute('title')
|
||||
|| trigger.textContent.trim(),
|
||||
);
|
||||
if (!hasAccessibleName && panel.textContent.trim()) {
|
||||
trigger.setAttribute('aria-label', panel.textContent.trim());
|
||||
}
|
||||
|
||||
if (!trigger.matches('a,button,input,select,textarea,[tabindex]')) {
|
||||
trigger.setAttribute('tabindex', '0');
|
||||
}
|
||||
};
|
||||
|
||||
const setOpenState = (state, isOpen) => {
|
||||
const { wrap, panel, trigger } = state;
|
||||
wrap.dataset.tooltipOpen = String(isOpen);
|
||||
panel.hidden = !isOpen;
|
||||
|
||||
if (trigger.matches('button,[role="button"],[aria-expanded]')) {
|
||||
trigger.setAttribute('aria-expanded', String(isOpen));
|
||||
}
|
||||
};
|
||||
|
||||
const restorePanel = (state) => {
|
||||
const { panel, placeholder } = state;
|
||||
panel.hidden = true;
|
||||
panel.removeAttribute('data-tooltip-portal');
|
||||
panel.removeAttribute('data-tooltip-placement');
|
||||
panel.style.removeProperty('left');
|
||||
panel.style.removeProperty('top');
|
||||
panel.style.removeProperty('visibility');
|
||||
panel.style.removeProperty('--tooltip-arrow-offset');
|
||||
|
||||
if (placeholder.parentNode) {
|
||||
placeholder.parentNode.insertBefore(panel, placeholder.nextSibling);
|
||||
}
|
||||
};
|
||||
|
||||
const movePanelToPortal = (state) => {
|
||||
const { panel } = state;
|
||||
if (panel.parentNode !== document.body) {
|
||||
document.body.appendChild(panel);
|
||||
}
|
||||
panel.setAttribute('data-tooltip-portal', 'true');
|
||||
panel.hidden = false;
|
||||
panel.style.visibility = 'hidden';
|
||||
};
|
||||
|
||||
const setArrowOffset = (state, placement, panelLeft, panelTop, panelWidth, panelHeight) => {
|
||||
const { trigger, panel } = state;
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const offset = placement === 'left' || placement === 'right'
|
||||
? triggerRect.top + (triggerRect.height / 2) - panelTop
|
||||
: triggerRect.left + (triggerRect.width / 2) - panelLeft;
|
||||
const maxOffset = placement === 'left' || placement === 'right' ? panelHeight : panelWidth;
|
||||
panel.style.setProperty('--tooltip-arrow-offset', `${Math.max(0, Math.min(offset, maxOffset))}px`);
|
||||
};
|
||||
|
||||
const positionTooltip = (state) => {
|
||||
const { trigger, panel } = state;
|
||||
if (panel.hidden || panel.parentNode !== document.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewport = getViewport();
|
||||
const safeInset = getPixelValue('--spacing-large');
|
||||
const gap = getPixelValue('--spacing-small');
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const panelWidth = panel.offsetWidth;
|
||||
const panelHeight = panel.offsetHeight;
|
||||
const centeredLeft = triggerRect.left + (triggerRect.width / 2) - (panelWidth / 2);
|
||||
const centeredRight = centeredLeft + panelWidth;
|
||||
const hasBottomSpace = triggerRect.bottom + gap + panelHeight <= viewport.height - safeInset;
|
||||
const hasTopSpace = triggerRect.top - gap - panelHeight >= safeInset;
|
||||
const hasLeftSpace = triggerRect.left - gap - panelWidth >= safeInset;
|
||||
const hasRightSpace = triggerRect.right + gap + panelWidth <= viewport.width - safeInset;
|
||||
|
||||
let placement = 'bottom';
|
||||
if (centeredRight > viewport.width - safeInset && hasLeftSpace) {
|
||||
placement = 'left';
|
||||
} else if (centeredLeft < safeInset && hasRightSpace) {
|
||||
placement = 'right';
|
||||
} else if (!hasBottomSpace && hasTopSpace) {
|
||||
placement = 'top';
|
||||
}
|
||||
|
||||
let panelLeft = centeredLeft;
|
||||
let panelTop = triggerRect.bottom + gap;
|
||||
|
||||
if (placement === 'top') {
|
||||
panelTop = triggerRect.top - gap - panelHeight;
|
||||
} else if (placement === 'left') {
|
||||
panelLeft = triggerRect.left - gap - panelWidth;
|
||||
panelTop = triggerRect.top + (triggerRect.height / 2) - (panelHeight / 2);
|
||||
} else if (placement === 'right') {
|
||||
panelLeft = triggerRect.right + gap;
|
||||
panelTop = triggerRect.top + (triggerRect.height / 2) - (panelHeight / 2);
|
||||
}
|
||||
|
||||
panelLeft = Math.max(safeInset, Math.min(panelLeft, viewport.width - safeInset - panelWidth));
|
||||
panelTop = Math.max(safeInset, Math.min(panelTop, viewport.height - safeInset - panelHeight));
|
||||
|
||||
panel.dataset.tooltipPlacement = placement;
|
||||
state.wrap.dataset.tooltipPlacement = placement;
|
||||
panel.style.left = `${Math.round(panelLeft)}px`;
|
||||
panel.style.top = `${Math.round(panelTop)}px`;
|
||||
setArrowOffset(state, placement, panelLeft, panelTop, panelWidth, panelHeight);
|
||||
panel.style.visibility = 'visible';
|
||||
};
|
||||
|
||||
const openTooltip = (state) => {
|
||||
tooltipStates.forEach((otherState) => {
|
||||
if (otherState !== state) {
|
||||
closeTooltip(otherState);
|
||||
}
|
||||
});
|
||||
movePanelToPortal(state);
|
||||
setOpenState(state, true);
|
||||
positionTooltip(state);
|
||||
};
|
||||
|
||||
const closeTooltip = (state) => {
|
||||
state.pinned = false;
|
||||
state.hoverTrigger = false;
|
||||
state.hoverPanel = false;
|
||||
state.focused = false;
|
||||
state.dismissed = false;
|
||||
setOpenState(state, false);
|
||||
restorePanel(state);
|
||||
};
|
||||
|
||||
const closeIfInactive = (state) => {
|
||||
if (!state.pinned && !state.hoverTrigger && !state.hoverPanel && !state.focused) {
|
||||
setOpenState(state, false);
|
||||
restorePanel(state);
|
||||
}
|
||||
};
|
||||
|
||||
const togglePinned = (state) => {
|
||||
if (state.pinned) {
|
||||
state.pinned = false;
|
||||
state.dismissed = true;
|
||||
setOpenState(state, false);
|
||||
restorePanel(state);
|
||||
return;
|
||||
}
|
||||
|
||||
state.pinned = true;
|
||||
state.dismissed = false;
|
||||
openTooltip(state);
|
||||
};
|
||||
|
||||
const initTooltip = (wrap) => {
|
||||
if (states.has(wrap)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = getElements(wrap);
|
||||
if (!elements.trigger || !elements.panel || !elements.panel.parentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const placeholder = document.createComment('sg-tooltip-panel-placeholder');
|
||||
elements.panel.parentNode.insertBefore(placeholder, elements.panel);
|
||||
|
||||
const state = {
|
||||
wrap,
|
||||
trigger: elements.trigger,
|
||||
panel: elements.panel,
|
||||
placeholder,
|
||||
pinned: false,
|
||||
hoverTrigger: false,
|
||||
hoverPanel: false,
|
||||
focused: false,
|
||||
dismissed: false,
|
||||
};
|
||||
wrap.dataset.tooltipInit = 'true';
|
||||
wrap.dataset.tooltipPlacement = 'bottom';
|
||||
states.set(wrap, state);
|
||||
tooltipStates.add(state);
|
||||
ensureAccessibility(state);
|
||||
setOpenState(state, false);
|
||||
|
||||
wrap.addEventListener('pointerenter', (event) => {
|
||||
if (event.pointerType !== 'mouse') {
|
||||
return;
|
||||
}
|
||||
if (state.dismissed) {
|
||||
return;
|
||||
}
|
||||
state.hoverTrigger = true;
|
||||
openTooltip(state);
|
||||
});
|
||||
|
||||
wrap.addEventListener('pointerleave', (event) => {
|
||||
if (event.pointerType !== 'mouse') {
|
||||
return;
|
||||
}
|
||||
state.hoverTrigger = false;
|
||||
state.dismissed = false;
|
||||
if (!state.panel.contains(event.relatedTarget)) {
|
||||
closeIfInactive(state);
|
||||
}
|
||||
});
|
||||
|
||||
state.panel.addEventListener('pointerenter', (event) => {
|
||||
if (event.pointerType !== 'mouse') {
|
||||
return;
|
||||
}
|
||||
if (!state.dismissed) {
|
||||
state.hoverPanel = true;
|
||||
openTooltip(state);
|
||||
}
|
||||
});
|
||||
|
||||
state.panel.addEventListener('pointerleave', (event) => {
|
||||
if (event.pointerType !== 'mouse') {
|
||||
return;
|
||||
}
|
||||
state.hoverPanel = false;
|
||||
state.dismissed = false;
|
||||
closeIfInactive(state);
|
||||
});
|
||||
|
||||
wrap.addEventListener('focusin', () => {
|
||||
if (!state.dismissed) {
|
||||
state.focused = true;
|
||||
openTooltip(state);
|
||||
}
|
||||
});
|
||||
|
||||
wrap.addEventListener('focusout', (event) => {
|
||||
if (wrap.contains(event.relatedTarget) || state.panel.contains(event.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
state.focused = false;
|
||||
state.dismissed = false;
|
||||
closeIfInactive(state);
|
||||
});
|
||||
|
||||
state.trigger.addEventListener('click', (event) => {
|
||||
togglePinned(state);
|
||||
});
|
||||
|
||||
state.trigger.addEventListener('keydown', (event) => {
|
||||
const isNativeInteractive = state.trigger.matches('a,button,input,select,textarea');
|
||||
if (!isNativeInteractive && (event.key === 'Enter' || event.key === ' ')) {
|
||||
event.preventDefault();
|
||||
togglePinned(state);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const initializeWithin = (root) => {
|
||||
if (root.nodeType === Node.DOCUMENT_NODE) {
|
||||
root.querySelectorAll('.sg-tooltip-wrap').forEach(initTooltip);
|
||||
return;
|
||||
}
|
||||
if (root.nodeType !== Node.ELEMENT_NODE) {
|
||||
return;
|
||||
}
|
||||
const owningWrap = root.closest('.sg-tooltip-wrap');
|
||||
if (owningWrap) {
|
||||
initTooltip(owningWrap);
|
||||
}
|
||||
if (root.matches('.sg-tooltip-wrap')) {
|
||||
initTooltip(root);
|
||||
}
|
||||
root.querySelectorAll('.sg-tooltip-wrap').forEach(initTooltip);
|
||||
};
|
||||
|
||||
const bootstrap = () => {
|
||||
window.sgInitTooltips = (options = {}) => {
|
||||
const root = options.root || document.body;
|
||||
initializeWithin(root);
|
||||
};
|
||||
|
||||
window.sgInitTooltips();
|
||||
|
||||
const observer = new MutationObserver((records) => {
|
||||
records.forEach((record) => {
|
||||
record.addedNodes.forEach(initializeWithin);
|
||||
});
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
const closeFromOutside = (event) => {
|
||||
tooltipStates.forEach((state) => {
|
||||
if (!state.wrap.contains(event.target) && !state.panel.contains(event.target)) {
|
||||
closeTooltip(state);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('pointerdown', closeFromOutside, true);
|
||||
document.addEventListener('touchstart', closeFromOutside, true);
|
||||
document.addEventListener('click', closeFromOutside, true);
|
||||
|
||||
const repositionOpenTooltips = () => {
|
||||
document.querySelectorAll('.sg-tooltip-wrap[data-tooltip-open="true"]').forEach((wrap) => {
|
||||
const state = states.get(wrap);
|
||||
if (state) {
|
||||
positionTooltip(state);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', repositionOpenTooltips);
|
||||
window.addEventListener('scroll', repositionOpenTooltips, true);
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
|
||||
} else {
|
||||
bootstrap();
|
||||
}
|
||||
})();
|
||||
@@ -224,6 +224,10 @@
|
||||
<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>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>
|
||||
<tr><td>icon-sandwich-line-portal</td><td>color-font-dark</td><td>Linienfarbe des Sandwich-Icons für große und kleine Variante.</td></tr>
|
||||
</tbody></table>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@import "./styles/03-typography-helpers.css";
|
||||
@import "./styles/10-components-interactive-elements.css";
|
||||
@import "./styles/47-patterns-file-upload.css";
|
||||
@import "./styles/48-patterns-history-graph.css";
|
||||
@import "./styles/20-patterns-portal-header.css";
|
||||
@import "./styles/21-patterns-options-row.css";
|
||||
@import "./styles/22-patterns-object-card.css";
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
--color-process-inactive: #FFAE79;
|
||||
--color-signal-green: #009101;
|
||||
--color-signal-green-light: #65A665;
|
||||
--color-signal-green-red: #C0624D;
|
||||
--color-signal-yellow: #9C7A00;
|
||||
--color-ampel-hellgelb: #D3B962;
|
||||
--color-signal-red: #9B3B2F;
|
||||
@@ -37,6 +38,7 @@
|
||||
--surface-card-header-muted: var(--color-darkbrown);
|
||||
--divider-card-segment: var(--color-white);
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-signal: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-dark);
|
||||
--text-card-transparent: var(--color-font-light);
|
||||
--layout-card-body-content-justify: flex-start;
|
||||
@@ -62,6 +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-pulldown-panel: var(--color-light-grey);
|
||||
--surface-activatable-remove: var(--surface-control-default);
|
||||
--surface-slider-track: var(--color-medium-grey);
|
||||
@@ -89,6 +92,8 @@
|
||||
--layout-mode-toggle-local-width-factor: 3;
|
||||
--layout-mode-toggle-width: calc(var(--interaction-height) * 3.5);
|
||||
--layout-help-panel-width: calc(var(--interaction-height) * 8);
|
||||
--layout-tooltip-width: calc(var(--interaction-height) * 8);
|
||||
--radius-tooltip: var(--radius-interaction);
|
||||
--text-control-default: var(--color-font-dark);
|
||||
--text-control-disabled: var(--color-dark-grey);
|
||||
--text-button-process: var(--color-font-light);
|
||||
@@ -98,6 +103,7 @@
|
||||
--text-menu-link-portal: var(--color-font-dark);
|
||||
--text-help-icon: var(--color-font-dark);
|
||||
--text-help-panel: var(--color-font-dark);
|
||||
--text-tooltip: var(--color-font-dark);
|
||||
--text-activatable-remove: var(--text-control-default);
|
||||
--text-hyperlink: var(--color-font-hyperlink);
|
||||
--icon-pulldown-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M2 2l4 4 4-4' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='square' stroke-linejoin='miter'/%3E%3C/svg%3E");
|
||||
@@ -140,7 +146,6 @@
|
||||
--layout-preview-align-items: flex-start;
|
||||
|
||||
/* Semantic pattern tokens: Portal header */
|
||||
--surface-portal-background: var(--color-background-purple);
|
||||
--surface-portal-header: var(--color-darkblue);
|
||||
--surface-portal-header-tab: var(--color-midblue);
|
||||
--surface-portal-header-tab-active: var(--color-light-grey);
|
||||
@@ -306,7 +311,6 @@
|
||||
--dimension-input-label-width: 9rem;
|
||||
--dimension-input-field-desktop-width: 400px;
|
||||
--dimension-input-field-max-width: 600px;
|
||||
--dimension-email-body-max-width: 600px;
|
||||
--dimension-search-field-width: 15.3rem;
|
||||
--dimension-public-content-width: 100%;
|
||||
--dimension-layer-pulldown-panel: 40;
|
||||
|
||||
@@ -8,7 +8,7 @@ body {
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-base);
|
||||
color: var(--color-font-dark);
|
||||
background: var(--surface-portal-background);
|
||||
background: var(--color-background-purple);
|
||||
margin: 0;
|
||||
padding: var(--spacing-small);
|
||||
}
|
||||
|
||||
@@ -1239,6 +1239,134 @@
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel {
|
||||
position: absolute;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
display: block;
|
||||
width: var(--layout-tooltip-width);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: anywhere;
|
||||
padding: var(--spacing-large);
|
||||
border-radius: var(--radius-tooltip);
|
||||
color: var(--text-tooltip);
|
||||
background: var(--surface-tooltip);
|
||||
box-shadow: var(--shadow-overlay);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel {
|
||||
top: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel {
|
||||
bottom: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
right: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
left: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel::before {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel::before {
|
||||
top: calc(var(--spacing-small) * -1);
|
||||
left: 50%;
|
||||
border-width: 0 var(--spacing-small) var(--spacing-small);
|
||||
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);
|
||||
left: 50%;
|
||||
border-width: var(--spacing-small) var(--spacing-small) 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);
|
||||
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;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"] {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="bottom"]::before {
|
||||
top: calc(var(--spacing-small) * -1);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent var(--surface-tooltip);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="top"]::before {
|
||||
bottom: calc(var(--spacing-small) * -1);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: var(--surface-tooltip) transparent transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="left"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
right: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent transparent var(--surface-tooltip);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="right"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
left: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-pulldown-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + var(--spacing-small));
|
||||
|
||||
@@ -91,19 +91,39 @@
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left {
|
||||
order: 0;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-pulldown-demo,
|
||||
.sg-options-row__left > .sg-input-single-line-wrap,
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 1 1 calc(50% - var(--layout-options-row-group-gap));
|
||||
.sg-options-row__left > .sg-pulldown-demo {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left:has(> .sg-pulldown-demo ~ .sg-pulldown-demo) > .sg-pulldown-demo {
|
||||
flex-basis: calc(50% - (var(--layout-options-row-group-gap) / 2));
|
||||
}
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left > .sg-pulldown-demo {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 0 0 auto;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row > .sg-search-field-input {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row .sg-input-single-line {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-input-single-line-wrap .sg-input-single-line,
|
||||
|
||||
@@ -136,22 +136,27 @@
|
||||
|
||||
.sg-card-segment--signal-red {
|
||||
background: var(--color-signal-red);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green {
|
||||
background: var(--color-signal-green);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-light {
|
||||
background: var(--color-signal-green-light);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-red {
|
||||
background: var(--color-signal-green-red);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-yellow {
|
||||
background: var(--color-signal-yellow);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--gray {
|
||||
@@ -171,6 +176,11 @@
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card.sg-card--notification-signal-green-red {
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card--content-card {
|
||||
--surface-card-header-primary: var(--surface-content-block-card-title);
|
||||
--surface-card-body: var(--surface-content-block-card-content);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/* ========================================================= */
|
||||
/* Pattern: History Graph */
|
||||
/* ========================================================= */
|
||||
|
||||
.sg-history-graph {
|
||||
--layout-history-graph-connector-width: var(--score-marker-width);
|
||||
--surface-history-graph-connector: var(--chart-marker-line);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sg-history-graph__event {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__event:not(:last-child)::after {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
width: var(--layout-history-graph-connector-width);
|
||||
height: var(--spacing-large);
|
||||
background: var(--surface-history-graph-connector);
|
||||
content: "";
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-history-graph__card {
|
||||
width: 100%;
|
||||
max-width: var(--layout-object-card-min-width);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.sg-history-graph__navigation {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -146,10 +146,12 @@ function create_direct_sales_order(PDO $pdo, array $fields): array
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO sales_order (
|
||||
id, external_ref, party_id, order_source, order_date, order_status, payment_status, payment_method_id,
|
||||
amount_net, amount_shipping, amount_tax, amount_discount, total_amount, currency, imported_at, created_at, updated_at
|
||||
amount_net, amount_shipping, amount_tax, amount_discount, total_amount, currency,
|
||||
market_occurrence_id, market_occurrence_day_id, imported_at, created_at, updated_at
|
||||
) VALUES (
|
||||
:id, :external_ref, :party_id, \'direct\', :order_date, \'imported\', \'paid\', :payment_method_id,
|
||||
:amount_net, 0, 0, 0, :total_amount, \'CHF\', NOW(), NOW(), NOW()
|
||||
:amount_net, 0, 0, 0, :total_amount, \'CHF\',
|
||||
:market_occurrence_id, :market_occurrence_day_id, NOW(), NOW(), NOW()
|
||||
)
|
||||
RETURNING id, external_ref'
|
||||
);
|
||||
@@ -161,6 +163,8 @@ function create_direct_sales_order(PDO $pdo, array $fields): array
|
||||
':payment_method_id' => $fields[':payment_method_id'] ?? null,
|
||||
':amount_net' => $fields[':amount_net'] ?? null,
|
||||
':total_amount' => $fields[':total_amount'] ?? null,
|
||||
':market_occurrence_id' => $fields[':market_occurrence_id'] ?? null,
|
||||
':market_occurrence_day_id' => $fields[':market_occurrence_day_id'] ?? null,
|
||||
]);
|
||||
|
||||
$row = $stmt->fetch();
|
||||
|
||||
@@ -41,6 +41,20 @@ $products = $data['products'];
|
||||
$orderDate = trim((string) $data['orderDate']);
|
||||
$payments = $data['payments'];
|
||||
$billing = is_array($data['billing']) ? $data['billing'] : [];
|
||||
$marketOccurrenceId = $data['marketOccurrenceId'] ?? null;
|
||||
$marketOccurrenceDayId = $data['marketOccurrenceDayId'] ?? null;
|
||||
|
||||
if (($marketOccurrenceId === null) !== ($marketOccurrenceDayId === null)) {
|
||||
json_response(422, ['ok' => false, 'error' => 'Markt und Markttag müssen gemeinsam angegeben werden.']);
|
||||
}
|
||||
|
||||
if ($marketOccurrenceId !== null && (!ctype_digit((string) $marketOccurrenceId) || (int) $marketOccurrenceId <= 0)) {
|
||||
json_response(422, ['ok' => false, 'error' => 'Ungültiger Markt.']);
|
||||
}
|
||||
|
||||
if ($marketOccurrenceDayId !== null && (!ctype_digit((string) $marketOccurrenceDayId) || (int) $marketOccurrenceDayId <= 0)) {
|
||||
json_response(422, ['ok' => false, 'error' => 'Ungültiger Markttag.']);
|
||||
}
|
||||
|
||||
if (!is_array($products) || count($products) === 0) {
|
||||
json_response(422, ['ok' => false, 'error' => 'No products specified']);
|
||||
@@ -115,6 +129,27 @@ try {
|
||||
$pdo = connect_database($env);
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($marketOccurrenceId !== null) {
|
||||
$marketLinkStmt = $pdo->prepare(
|
||||
'SELECT 1
|
||||
FROM market_occurrence o
|
||||
INNER JOIN market_definition m ON m.id = o.market_definition_id
|
||||
INNER JOIN market_occurrence_day d ON d.occurrence_id = o.id
|
||||
WHERE o.id = :occurrence_id
|
||||
AND d.id = :day_id
|
||||
AND m.archived_at IS NULL
|
||||
AND o.archived_at IS NULL
|
||||
LIMIT 1'
|
||||
);
|
||||
$marketLinkStmt->execute([
|
||||
':occurrence_id' => (int) $marketOccurrenceId,
|
||||
':day_id' => (int) $marketOccurrenceDayId,
|
||||
]);
|
||||
if ($marketLinkStmt->fetchColumn() === false) {
|
||||
throw new InvalidArgumentException('Der Markt oder der Markttag ist nicht aktiv oder nicht miteinander verknüpft.');
|
||||
}
|
||||
}
|
||||
|
||||
$locations = get_default_location_ids($pdo);
|
||||
|
||||
$paymentMethodIds = [];
|
||||
@@ -135,6 +170,8 @@ try {
|
||||
':amount_net' => $totalPrice,
|
||||
':total_amount' => $totalPrice,
|
||||
':order_date' => $orderDateTime->format('Y-m-d H:i:s'),
|
||||
':market_occurrence_id' => $marketOccurrenceId === null ? null : (int) $marketOccurrenceId,
|
||||
':market_occurrence_day_id' => $marketOccurrenceDayId === null ? null : (int) $marketOccurrenceDayId,
|
||||
]);
|
||||
|
||||
$orderId = (int) $order['id'];
|
||||
@@ -221,7 +258,16 @@ try {
|
||||
'excelTrigger' => $excelTrigger,
|
||||
'message' => 'OTC order created successfully',
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
} catch (InvalidArgumentException $e) {
|
||||
if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
json_response(422, [
|
||||
'ok' => false,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
@@ -125,6 +125,101 @@ function get_manufacturer_options(PDO $pdo): array
|
||||
);
|
||||
}
|
||||
|
||||
function get_party_reference(PDO $pdo, int $partyId): ?array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT id, name, email, phone
|
||||
FROM party
|
||||
WHERE id = :id'
|
||||
);
|
||||
$stmt->execute([':id' => $partyId]);
|
||||
$party = $stmt->fetch();
|
||||
|
||||
return is_array($party) ? $party : null;
|
||||
}
|
||||
|
||||
function get_contact_reference(PDO $pdo, int $contactId): ?array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT id, party_id, first_name, last_name, email, phone, position_title
|
||||
FROM contact
|
||||
WHERE id = :id'
|
||||
);
|
||||
$stmt->execute([':id' => $contactId]);
|
||||
$contact = $stmt->fetch();
|
||||
|
||||
return is_array($contact) ? $contact : null;
|
||||
}
|
||||
|
||||
function get_party_options(PDO $pdo): array
|
||||
{
|
||||
$stmt = $pdo->query(
|
||||
"SELECT id, name
|
||||
FROM party
|
||||
WHERE status = 'active'
|
||||
ORDER BY name ASC, id ASC"
|
||||
);
|
||||
|
||||
return array_map(
|
||||
static fn (array $row): array => ['id' => (int) $row['id'], 'name' => (string) ($row['name'] ?? '')],
|
||||
$stmt->fetchAll()
|
||||
);
|
||||
}
|
||||
|
||||
function get_contact_options(PDO $pdo): array
|
||||
{
|
||||
$stmt = $pdo->query(
|
||||
"SELECT c.id, c.party_id, c.first_name, c.last_name, p.name AS party_name
|
||||
FROM contact c
|
||||
INNER JOIN party p ON p.id = c.party_id
|
||||
WHERE p.status = 'active'
|
||||
ORDER BY p.name ASC, c.last_name ASC, c.first_name ASC, c.id ASC"
|
||||
);
|
||||
|
||||
return array_map(
|
||||
static fn (array $row): array => [
|
||||
'id' => (int) $row['id'],
|
||||
'party_id' => (int) $row['party_id'],
|
||||
'first_name' => (string) ($row['first_name'] ?? ''),
|
||||
'last_name' => (string) ($row['last_name'] ?? ''),
|
||||
'party_name' => (string) ($row['party_name'] ?? ''),
|
||||
],
|
||||
$stmt->fetchAll()
|
||||
);
|
||||
}
|
||||
|
||||
function update_contact_details(PDO $pdo, int $contactId, array $input): array
|
||||
{
|
||||
if ($contactId <= 0) {
|
||||
throw new InvalidArgumentException('contact_person_id muss positiv sein.');
|
||||
}
|
||||
|
||||
$sets = [];
|
||||
$params = [':id' => $contactId];
|
||||
foreach (['email', 'phone'] as $field) {
|
||||
if (array_key_exists($field, $input)) {
|
||||
$sets[] = $field . ' = :' . $field;
|
||||
$value = trim((string) ($input[$field] ?? ''));
|
||||
$params[':' . $field] = $value !== '' ? $value : null;
|
||||
}
|
||||
}
|
||||
if ($sets === []) {
|
||||
throw new InvalidArgumentException('Keine änderbaren Kontaktdaten übergeben.');
|
||||
}
|
||||
|
||||
$sets[] = 'updated_at = NOW()';
|
||||
$stmt = $pdo->prepare(
|
||||
'UPDATE contact SET ' . implode(', ', $sets) . ' WHERE id = :id
|
||||
RETURNING id, party_id, first_name, last_name, email, phone, position_title'
|
||||
);
|
||||
$stmt->execute($params);
|
||||
$contact = $stmt->fetch();
|
||||
if (!is_array($contact)) {
|
||||
throw new RuntimeException('Kontaktperson nicht gefunden.');
|
||||
}
|
||||
return $contact;
|
||||
}
|
||||
|
||||
function create_manufacturer(PDO $pdo, string $name, ?string $email = null, ?string $phone = null): array
|
||||
{
|
||||
$name = trim($name);
|
||||
|
||||
@@ -30,30 +30,68 @@ function market_management_mcp_authorize(array $env): string
|
||||
|
||||
function market_management_mcp_tools(): array
|
||||
{
|
||||
$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'],
|
||||
'confirmation_date' => ['type' => ['string', 'null'], 'format' => 'date'],
|
||||
'payment_deadline' => ['type' => ['string', 'null'], 'format' => 'date'],
|
||||
'payment_status' => ['type' => ['string', 'null'], 'enum' => ['paid', 'unpaid']],
|
||||
'payment_date' => ['type' => ['string', 'null'], 'format' => 'date'], 'paid_amount' => ['type' => ['number', 'null']],
|
||||
'payment_method_code' => ['type' => ['string', 'null']], 'stand_size' => ['type' => ['string', 'null']],
|
||||
'gross_cost' => ['type' => ['number', 'null'], 'minimum' => 0], 'tax_status' => ['type' => ['string', 'null'], 'enum' => ['tax_free', 'taxable']], 'tax_rate' => ['type' => ['number', 'null'], 'minimum' => 0], 'cost_items_text' => ['type' => ['string', 'null']],
|
||||
'table_available' => ['type' => ['boolean', 'null']], 'table_rentable' => ['type' => ['boolean', 'null']],
|
||||
'own_table_required' => ['type' => ['boolean', 'null']], 'shelter_available' => ['type' => ['boolean', 'null']],
|
||||
'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']],
|
||||
'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']],
|
||||
'revenue_share_terms' => ['type' => ['string', 'null']], 'other_participation_conditions' => ['type' => ['string', 'null']],
|
||||
'contract_folder_url' => ['type' => ['string', 'null'], 'format' => 'uri'],
|
||||
'contact_person_name' => ['type' => ['string', 'null']],
|
||||
'contact_email' => ['type' => ['string', 'null'], 'format' => 'email'], 'contact_phone' => ['type' => ['string', 'null']],
|
||||
];
|
||||
$dayProperties = [
|
||||
'date' => ['type' => 'string', 'format' => 'date'], 'observations' => ['type' => ['string', 'null']],
|
||||
'stand_start_time' => ['type' => ['string', 'null'], 'format' => 'time'], 'stand_end_time' => ['type' => ['string', 'null'], 'format' => 'time'],
|
||||
'setup_start_time' => ['type' => ['string', 'null'], 'format' => 'time'], 'setup_end_time' => ['type' => ['string', 'null'], 'format' => 'time'],
|
||||
'delivery_start_time' => ['type' => ['string', 'null'], 'format' => 'time'], 'delivery_end_time' => ['type' => ['string', 'null'], 'format' => 'time'],
|
||||
'inherit_from_first_day' => ['type' => ['boolean', 'null']],
|
||||
];
|
||||
$searchProperties = [
|
||||
'search' => ['type' => 'string'], 'market_definition_id' => ['type' => 'integer'], 'category_id' => ['type' => 'integer'],
|
||||
'status' => ['type' => 'string'], 'place' => ['type' => 'string'], 'address' => ['type' => 'string'],
|
||||
'municipality_reference_id' => ['type' => 'integer'], 'date_from' => ['type' => 'string', 'format' => 'date'],
|
||||
'date_to' => ['type' => 'string', 'format' => 'date'], 'include_archived' => ['type' => 'boolean'],
|
||||
'sort' => ['type' => 'string'], 'sort_direction' => ['type' => 'string', 'enum' => ['asc', 'desc']],
|
||||
'limit' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100], 'offset' => ['type' => 'integer', 'minimum' => 0],
|
||||
];
|
||||
return [
|
||||
['name' => 'market_management.search_markets', 'description' => 'Märkte suchen', 'inputSchema' => ['type' => 'object']],
|
||||
['name' => 'market_management.search_occurrences', 'description' => 'Durchführungen suchen und auflisten', 'inputSchema' => ['type' => 'object']],
|
||||
['name' => 'market_management.get_market', 'description' => 'Markt mit Durchführungen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]],
|
||||
['name' => 'market_management.create_market', 'description' => 'Markt mit bestehender Kategorie und optionaler Website-URL anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'category_id'], 'properties' => ['name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]],
|
||||
['name' => 'market_management.update_market', 'description' => 'Markt inklusive Website-URL ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer'], 'name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]],
|
||||
['name' => 'market_management.archive_market', 'description' => 'Markt archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]],
|
||||
['name' => 'market_management.reactivate_market', 'description' => 'Markt reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]],
|
||||
['name' => 'market_management.list_categories', 'description' => 'Bestehende Kategorien lesen', 'inputSchema' => ['type' => 'object']],
|
||||
['name' => 'market_management.list_reference_data', 'description' => 'Kategorien, Kantone und Gemeinden lesen', 'inputSchema' => ['type' => 'object']],
|
||||
['name' => 'market_management.get_overview', 'description' => 'Marktübersicht mit serverseitigen Filtern lesen', 'inputSchema' => ['type' => 'object']],
|
||||
['name' => 'market_management.create_occurrence', 'description' => 'Durchführung mit mindestens einem Tag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id', 'days']]],
|
||||
['name' => 'market_management.get_occurrence', 'description' => 'Durchführung mit Tagen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]],
|
||||
['name' => 'market_management.update_occurrence', 'description' => 'Durchführung ändern', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]],
|
||||
['name' => 'market_management.archive_occurrence', 'description' => 'Durchführung archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]],
|
||||
['name' => 'market_management.reactivate_occurrence', 'description' => 'Durchführung reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]],
|
||||
['name' => 'market_management.create_day', 'description' => 'Durchführungstag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'date']]],
|
||||
['name' => 'market_management.update_day', 'description' => 'Durchführungstag ändern', 'inputSchema' => ['type' => 'object', 'required' => ['day_id']]],
|
||||
['name' => 'market_management.delete_day', 'description' => 'Durchführungstag entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['day_id']]],
|
||||
['name' => 'market_management.set_organizer', 'description' => 'Veranstalter einer Durchführung setzen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'organization_contact_id', 'contact_person_id']]],
|
||||
['name' => 'market_management.delete_organizer', 'description' => 'Veranstalter einer Durchführung entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]],
|
||||
['name' => 'market_management.create_cost_item', 'description' => 'Kostenposition anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'cost_type', 'gross_amount', 'tax_status']]],
|
||||
['name' => 'market_management.update_cost_item', 'description' => 'Kostenposition ändern', 'inputSchema' => ['type' => 'object', 'required' => ['cost_item_id']]],
|
||||
['name' => 'market_management.delete_cost_item', 'description' => 'Kostenposition entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['cost_item_id']]],
|
||||
['name' => 'market_management.search_markets', 'description' => 'Märkte suchen', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]],
|
||||
['name' => 'market_management.search_occurrences', 'description' => 'Marktdurchführungen suchen und auflisten', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]],
|
||||
['name' => 'market_management.get_market', 'description' => 'Markt mit Marktdurchführungen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]],
|
||||
['name' => 'market_management.create_market', 'description' => 'Markt mit verpflichtendem Veranstalter, Kategorie und optionaler Website-URL anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'category_id', 'organizer_id'], 'properties' => ['name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]],
|
||||
['name' => 'market_management.update_market', 'description' => 'Markt inklusive Veranstalter und Website-URL ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer'], 'name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]],
|
||||
['name' => 'market_management.archive_market', 'description' => 'Markt archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer']]]],
|
||||
['name' => 'market_management.reactivate_market', 'description' => 'Markt reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer']]]],
|
||||
['name' => 'market_management.list_categories', 'description' => 'Bestehende Kategorien lesen', 'inputSchema' => ['type' => 'object', 'properties' => (object) []]],
|
||||
['name' => 'market_management.list_market_organizers', 'description' => 'Aktive Veranstalter lesen', 'inputSchema' => ['type' => 'object', 'properties' => (object) []]],
|
||||
['name' => 'market_management.create_market_organizer', 'description' => 'Veranstalter mit optionalen Kontaktdaten anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name'], 'properties' => ['name' => ['type' => 'string'], 'email' => ['type' => ['string', 'null'], 'format' => 'email'], 'phone' => ['type' => ['string', 'null']]]]],
|
||||
['name' => 'market_management.list_reference_data', 'description' => 'Kategorien, Kantone und Gemeinden lesen', 'inputSchema' => ['type' => 'object', 'properties' => (object) []]],
|
||||
['name' => 'market_management.create_municipality', 'description' => 'Lokale Gemeinde im Markt-Gemeinderegister anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name'], 'properties' => ['name' => ['type' => 'string']]]],
|
||||
['name' => 'market_management.get_overview', 'description' => 'Marktübersicht mit serverseitigen Filtern lesen', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]],
|
||||
['name' => 'market_management.create_occurrence', 'description' => 'Marktdurchführung mit mindestens einem Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id', 'days'], 'properties' => ['market_definition_id' => ['type' => 'integer'], 'status' => ['type' => 'string'], 'days' => ['type' => 'array', 'minItems' => 1, 'items' => ['type' => 'object', 'required' => ['date'], 'properties' => $dayProperties]] + $occurrenceProperties]]],
|
||||
['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']]]],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -61,13 +99,12 @@ function market_management_mcp_is_write_tool(string $tool): bool
|
||||
{
|
||||
return in_array($tool, [
|
||||
'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.archive_occurrence', 'market_management.reactivate_occurrence',
|
||||
'market_management.create_day', 'market_management.update_day', 'market_management.delete_day',
|
||||
'market_management.set_organizer', 'market_management.delete_organizer',
|
||||
'market_management.create_cost_item', 'market_management.update_cost_item',
|
||||
'market_management.delete_cost_item',
|
||||
], true);
|
||||
}
|
||||
|
||||
@@ -146,16 +183,83 @@ function market_management_mcp_call_idempotent(PDO $pdo, string $userSubject, st
|
||||
}
|
||||
}
|
||||
|
||||
function market_management_mcp_get_occurrence(PDO $pdo, int $occurrenceId, bool $includeArchived = false): array
|
||||
{
|
||||
$occurrence = market_management_get_occurrence($pdo, $occurrenceId, $includeArchived);
|
||||
if ($occurrence === null) {
|
||||
throw new RuntimeException('Marktdurchführung nicht gefunden.');
|
||||
}
|
||||
$market = market_management_get_market($pdo, (int) $occurrence['market_definition_id'], $includeArchived);
|
||||
if ($market === null) {
|
||||
throw new RuntimeException('Markt nicht gefunden.');
|
||||
}
|
||||
unset($market['occurrences']);
|
||||
$occurrence['market'] = $market;
|
||||
return $occurrence;
|
||||
}
|
||||
|
||||
function market_management_mcp_get_market(PDO $pdo, int $marketId, bool $includeArchived = false): array
|
||||
{
|
||||
$market = market_management_get_market($pdo, $marketId, $includeArchived);
|
||||
if ($market === null) {
|
||||
throw new RuntimeException('Markt nicht gefunden.');
|
||||
}
|
||||
foreach ($market['occurrences'] as $index => $occurrence) {
|
||||
$enriched = market_management_get_occurrence($pdo, (int) $occurrence['id'], $includeArchived);
|
||||
if ($enriched !== null) {
|
||||
$market['occurrences'][$index] = $enriched;
|
||||
}
|
||||
}
|
||||
return $market;
|
||||
}
|
||||
|
||||
function market_management_mcp_update_occurrence(PDO $pdo, int $occurrenceId, array $input): array
|
||||
{
|
||||
unset($input['occurrence_id']);
|
||||
$simpleCostFields = [];
|
||||
foreach (['gross_cost', 'cost_items_text', 'tax_status', 'tax_rate'] as $field) {
|
||||
if (array_key_exists($field, $input)) {
|
||||
$simpleCostFields[$field] = $input[$field];
|
||||
unset($input[$field]);
|
||||
}
|
||||
}
|
||||
if ($simpleCostFields !== []) {
|
||||
market_management_update_occurrence_simple_costs($pdo, $occurrenceId, $simpleCostFields);
|
||||
}
|
||||
$contactFields = [];
|
||||
foreach (['contact_person_name', 'contact_email', 'contact_phone'] as $field) {
|
||||
if (array_key_exists($field, $input)) {
|
||||
$contactFields[$field] = $input[$field];
|
||||
unset($input[$field]);
|
||||
}
|
||||
}
|
||||
$existing = null;
|
||||
if ($contactFields !== []) {
|
||||
$existing = market_management_get_occurrence($pdo, $occurrenceId, true);
|
||||
if ($existing === null) {
|
||||
throw new RuntimeException('Marktdurchführung nicht gefunden.');
|
||||
}
|
||||
}
|
||||
if ($contactFields !== []) {
|
||||
$contactFields['occurrence_id'] = $occurrenceId;
|
||||
market_management_set_occurrence_contact($pdo, $contactFields);
|
||||
}
|
||||
if ($input !== []) {
|
||||
market_management_update_occurrence($pdo, $occurrenceId, $input);
|
||||
}
|
||||
return market_management_mcp_get_occurrence($pdo, $occurrenceId, true);
|
||||
}
|
||||
|
||||
function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): array
|
||||
{
|
||||
return match ($tool) {
|
||||
'market_management.search_markets' => market_management_search_markets($pdo, $arguments),
|
||||
'market_management.search_occurrences' => market_management_search_occurrences($pdo, $arguments),
|
||||
'market_management.get_market' => market_management_get_market(
|
||||
'market_management.get_market' => market_management_mcp_get_market(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'),
|
||||
(bool) ($arguments['include_archived'] ?? false)
|
||||
) ?? throw new RuntimeException('Markt nicht gefunden.'),
|
||||
),
|
||||
'market_management.create_market' => market_management_create_market($pdo, $arguments),
|
||||
'market_management.update_market' => market_management_update_market(
|
||||
$pdo,
|
||||
@@ -173,18 +277,32 @@ function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): a
|
||||
false
|
||||
),
|
||||
'market_management.list_categories' => market_management_list_categories($pdo),
|
||||
'market_management.list_market_organizers' => market_management_list_organizers($pdo),
|
||||
'market_management.create_market_organizer' => market_management_create_organizer(
|
||||
$pdo,
|
||||
(string) ($arguments['name'] ?? ''),
|
||||
$arguments['email'] ?? null,
|
||||
$arguments['phone'] ?? null
|
||||
),
|
||||
'market_management.list_reference_data' => market_management_list_reference_data($pdo, $arguments),
|
||||
'market_management.create_municipality' => market_management_create_municipality(
|
||||
$pdo,
|
||||
(string) ($arguments['name'] ?? '')
|
||||
),
|
||||
'market_management.get_overview' => [
|
||||
'markets' => market_management_search_markets($pdo, $arguments),
|
||||
'occurrences' => market_management_search_occurrences($pdo, $arguments),
|
||||
],
|
||||
'market_management.create_occurrence' => market_management_create_occurrence($pdo, $arguments),
|
||||
'market_management.get_occurrence' => market_management_get_occurrence(
|
||||
'market_management.create_occurrence' => (static function () use ($pdo, $arguments): array {
|
||||
$occurrence = market_management_create_occurrence($pdo, $arguments);
|
||||
return $occurrence;
|
||||
})(),
|
||||
'market_management.get_occurrence' => market_management_mcp_get_occurrence(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
|
||||
(bool) ($arguments['include_archived'] ?? false)
|
||||
) ?? throw new RuntimeException('Durchführung nicht gefunden.'),
|
||||
'market_management.update_occurrence' => market_management_update_occurrence(
|
||||
),
|
||||
'market_management.update_occurrence' => market_management_mcp_update_occurrence(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
|
||||
$arguments
|
||||
@@ -209,21 +327,6 @@ function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): a
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['day_id'] ?? null, 'day_id')
|
||||
),
|
||||
'market_management.set_organizer' => market_management_set_organizer($pdo, $arguments),
|
||||
'market_management.delete_organizer' => market_management_delete_organizer(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id')
|
||||
),
|
||||
'market_management.create_cost_item' => market_management_create_cost_item($pdo, $arguments),
|
||||
'market_management.update_cost_item' => market_management_update_cost_item(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['cost_item_id'] ?? null, 'cost_item_id'),
|
||||
$arguments
|
||||
),
|
||||
'market_management.delete_cost_item' => market_management_delete_cost_item(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['cost_item_id'] ?? null, 'cost_item_id')
|
||||
),
|
||||
default => throw new InvalidArgumentException('Unbekanntes MCP-Tool: ' . $tool),
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+354
-82
@@ -97,14 +97,16 @@ function auth_render_markets_large_table(array $marketTable): string
|
||||
$pageSize = (int) ($marketTable['page_size'] ?? 20);
|
||||
$hasMore = (bool) ($marketTable['has_more'] ?? (count($rows) >= $limit));
|
||||
$sortColumns = [
|
||||
'market_name' => 'Name der Veranstaltung',
|
||||
'earliest_date' => 'Durchführung',
|
||||
'market_name' => 'Markt',
|
||||
'earliest_date' => 'Markttag(e)',
|
||||
'place' => 'Ort',
|
||||
'registration_deadline' => 'Anmeldefrist',
|
||||
'status' => 'Status',
|
||||
];
|
||||
|
||||
$html = [];
|
||||
$html[] = '<section class="sg-card-list-page" aria-label="Märkte">';
|
||||
$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[] = '<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>';
|
||||
@@ -124,6 +126,7 @@ function auth_render_markets_large_table(array $marketTable): string
|
||||
$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>';
|
||||
$headerIndex++;
|
||||
}
|
||||
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader">Verkaufte Produkte</div>';
|
||||
$html[] = '</div>';
|
||||
|
||||
foreach ($rows as $index => $row) {
|
||||
@@ -135,10 +138,23 @@ function auth_render_markets_large_table(array $marketTable): string
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html((string) ($row['market_name'] ?? '')) . '</div>';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell"><a class="sg-hyperlink" href="#" aria-haspopup="dialog" aria-expanded="false" data-market-drawer-open="true" data-market-occurrence-id="' . (int) ($row['id'] ?? 0) . '">' . auth_escape_html(auth_format_market_date($row['earliest_date'] ?? null, $row['latest_date'] ?? null)) . '</a></div>';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html((string) ($row['place'] ?? '')) . '</div>';
|
||||
$registrationDeadline = (string) ($row['registration_deadline'] ?? '');
|
||||
$registrationDeadlineLabel = '';
|
||||
if ($registrationDeadline !== '') {
|
||||
try {
|
||||
$registrationDeadlineLabel = (new DateTimeImmutable($registrationDeadline))->format('d.m.Y');
|
||||
} catch (Throwable) {
|
||||
$registrationDeadlineLabel = $registrationDeadline;
|
||||
}
|
||||
}
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html($registrationDeadlineLabel) . '</div>';
|
||||
$status = (string) ($row['status'] ?? '');
|
||||
$statusSignalClass = trim(auth_market_status_signal_class($status));
|
||||
$statusValue = '<span' . ($statusSignalClass !== '' ? ' class="' . auth_escape_html($statusSignalClass) . '"' : '') . '>' . auth_escape_html(auth_market_status_label($status)) . '</span>';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . $statusValue . '</div>';
|
||||
$soldItemCount = (int) round((float) ($row['sold_item_count'] ?? 0));
|
||||
$soldGrossRevenue = number_format((float) ($row['sold_gross_revenue'] ?? 0), 0, '.', "'");
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . $soldItemCount . '/' . $soldGrossRevenue . 'CHF</div>';
|
||||
$html[] = '</div>';
|
||||
}
|
||||
|
||||
@@ -151,14 +167,63 @@ function auth_render_markets_large_table(array $marketTable): string
|
||||
|
||||
function auth_render_markets_content(array $marketTable): string
|
||||
{
|
||||
return '<div data-markets-content="true" data-market-csrf-token="' . auth_escape_html(auth_csrf_token()) . '">' . auth_render_markets_large_table($marketTable)
|
||||
return '<div data-markets-content="true" data-market-csrf-token="' . auth_escape_html(auth_csrf_token()) . '">'
|
||||
. auth_render_markets_large_table($marketTable)
|
||||
. '<div class="sg-card-list-page-drawer-backdrop" aria-hidden="true" data-market-drawer-backdrop="true" data-open="false"></div>'
|
||||
. '<aside class="sg-card-list-page-drawer" aria-label="Marktdurchführung-Details" aria-hidden="true" data-market-drawer="true" data-open="false">'
|
||||
. '<div class="sg-card-list-page-drawer__header"><h2 class="sg-heading-h2 sg-card-list-page-drawer__title" data-market-drawer-title>Marktdurchführung</h2></div>'
|
||||
. '<div class="sg-card-list-page-drawer__content" data-market-drawer-content><p class="sg-body">Marktdetails werden geladen …</p></div>'
|
||||
. '</aside></div>';
|
||||
}
|
||||
|
||||
function auth_render_market_definitions_content(array $marketTable): string
|
||||
{
|
||||
$rows = $marketTable['rows'] ?? $marketTable;
|
||||
$categories = $marketTable['categories'] ?? [];
|
||||
$organizers = $marketTable['organizers'] ?? [];
|
||||
$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[] = '<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>';
|
||||
}
|
||||
$html[] = '</div>';
|
||||
foreach ($rows as $index => $row) {
|
||||
$rowClass = 'sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row';
|
||||
if ($index % 2 === 1) {
|
||||
$rowClass .= ' sg-large-table__row--striped-light';
|
||||
}
|
||||
$website = (string) ($row['website_url'] ?? '');
|
||||
$websiteValue = $website === '' ? 'Nicht hinterlegt' : $website;
|
||||
$status = ($row['archived_at'] ?? null) === null ? 'Aktiv' : 'Archiviert';
|
||||
$marketId = (int) ($row['id'] ?? 0);
|
||||
$marketName = (string) ($row['name'] ?? '');
|
||||
$organizerName = (string) ($row['organizer_name'] ?? '');
|
||||
$html[] = '<div class="' . $rowClass . '" role="row" data-component-part="large-table-row" data-market-definition-row data-market-definition-id="' . $marketId . '">';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell"><a class="sg-hyperlink" href="#" aria-haspopup="dialog" aria-expanded="false" data-market-definition-open data-market-definition-id="' . $marketId . '" data-market-definition-name="' . auth_escape_html($marketName) . '" data-market-definition-category-id="' . (int) ($row['category_id'] ?? 0) . '" data-market-definition-category-name="' . auth_escape_html((string) ($row['category_name'] ?? $row['category_code'] ?? '')) . '" data-market-definition-organizer-id="' . (int) ($row['organizer_id'] ?? 0) . '" data-market-definition-organizer-name="' . auth_escape_html($organizerName) . '" data-market-definition-website="' . auth_escape_html($website) . '">' . auth_escape_html($marketName) . '</a></div>';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html((string) ($row['category_name'] ?? $row['category_code'] ?? '')) . '</div>';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html($organizerName) . '</div>';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html($websiteValue) . '</div>';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html((string) ($row['occurrence_count'] ?? 0)) . '</div>';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html($status) . '</div>';
|
||||
$html[] = '</div>';
|
||||
}
|
||||
$html[] = '</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) {
|
||||
$html[] = '<option value="' . (int) ($category['id'] ?? 0) . '">' . auth_escape_html((string) ($category['name'] ?? $category['code'] ?? '')) . '</option>';
|
||||
}
|
||||
$html[] = '</select></label></div><div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Veranstalter<span class="market-required-field-indicator" aria-hidden="true">*</span></span><select class="sg-interaction-element sg-input-single-line sg-pulldown" name="organizer_id" required data-market-definition-organizer><option value="">Bitte auswählen</option>';
|
||||
foreach ($organizers as $organizer) {
|
||||
$html[] = '<option value="' . (int) ($organizer['id'] ?? 0) . '">' . auth_escape_html((string) ($organizer['name'] ?? '')) . '</option>';
|
||||
}
|
||||
$html[] = '<option value="__new__">Neuen Veranstalter anlegen</option></select></label></div><div class="sg-form-sections-card__field-group" data-market-definition-new-organizer hidden><label class="sg-labeled-input-row"><span class="sg-label">Name des neuen Veranstalters<span class="market-required-field-indicator" aria-hidden="true">*</span></span><input class="sg-interaction-element sg-input-single-line" type="text" name="organizer_name" data-market-definition-organizer-name></label><label class="sg-labeled-input-row"><span class="sg-label">E-Mail</span><input class="sg-interaction-element sg-input-single-line" type="email" name="organizer_email" data-market-definition-organizer-email></label><label class="sg-labeled-input-row"><span class="sg-label">Telefon</span><input class="sg-interaction-element sg-input-single-line" type="tel" name="organizer_phone" data-market-definition-organizer-phone></label></div><div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Website</span><input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="url" name="website_url" placeholder="https://" data-market-definition-website></label></div></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-definition-cancel 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-definition-status aria-live="polite"></p></form></div></div></div></article></div></aside></div>';
|
||||
return implode('', $html);
|
||||
}
|
||||
|
||||
function auth_render_bestellungen_large_table(array $bestellungenTable): string
|
||||
{
|
||||
$rows = $bestellungenTable['rows'] ?? [];
|
||||
@@ -440,17 +505,17 @@ function auth_render_lager_historical_lots(array $lagerProducts): string
|
||||
$emptyDate = $qtyNet <= 0 ? auth_format_lager_date($lot['last_out_at'] ?? null) : 'Nicht eindeutig';
|
||||
$html[] = '<article class="sg-card sg-card--content-card" data-component="content-card" aria-label="Vergangene Charge">'
|
||||
. '<div 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">Vergangene Charge</div></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body">'
|
||||
. '<p class="sg-body"><strong>Naurua-Chargennummer:</strong> ' . auth_escape_html((string) ($lot['lot_number'] ?? 'Nicht hinterlegt')) . '<br>'
|
||||
. '<strong>Status:</strong> Abgeschlossen<br>'
|
||||
. '<strong>Eingangsmenge:</strong> ' . auth_format_lager_quantity((float) ($lot['qty_in'] ?? 0)) . '<br>'
|
||||
. '<strong>Verbrauchte Menge:</strong> ' . auth_format_lager_quantity((float) ($lot['qty_out'] ?? 0)) . '<br>'
|
||||
. '<strong>Verbrauchszeitraum:</strong> ' . auth_format_lager_date($lot['first_in_at'] ?? null) . ' – ' . auth_format_lager_date($lot['last_out_at'] ?? null) . '<br>'
|
||||
. '<strong>Aufgebraucht am:</strong> ' . $emptyDate . '<br>'
|
||||
. '<strong>MHD:</strong> ' . auth_format_lager_date($lot['expiry_date'] ?? null) . '<br>'
|
||||
. '<strong>Hersteller:</strong> ' . auth_escape_html((string) ($lot['manufacturer_name'] ?? 'Nicht hinterlegt')) . '<br>'
|
||||
. '<strong>Hersteller-Chargennummer:</strong> ' . auth_escape_html((string) ($lot['manufacturer_lot_number'] ?? 'Nicht hinterlegt')) . '</p>'
|
||||
. '</div></article>';
|
||||
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body"><dl class="market-detail-grid">'
|
||||
. '<div class="market-detail-row"><dt>Naurua-Chargennummer</dt><dd>' . auth_escape_html((string) ($lot['lot_number'] ?? 'Nicht hinterlegt')) . '</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>Status</dt><dd>Abgeschlossen</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>Eingangsmenge</dt><dd>' . auth_format_lager_quantity((float) ($lot['qty_in'] ?? 0)) . '</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>Verbrauchte Menge</dt><dd>' . auth_format_lager_quantity((float) ($lot['qty_out'] ?? 0)) . '</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>Verbrauchszeitraum</dt><dd>' . auth_format_lager_date($lot['first_in_at'] ?? null) . ' – ' . auth_format_lager_date($lot['last_out_at'] ?? null) . '</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>Aufgebraucht am</dt><dd>' . $emptyDate . '</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>MHD</dt><dd>' . auth_format_lager_date($lot['expiry_date'] ?? null) . '</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>Hersteller</dt><dd>' . auth_escape_html((string) ($lot['manufacturer_name'] ?? 'Nicht hinterlegt')) . '</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>Hersteller-Chargennummer</dt><dd>' . auth_escape_html((string) ($lot['manufacturer_lot_number'] ?? 'Nicht hinterlegt')) . '</dd></div>'
|
||||
. '</dl></div></article>';
|
||||
}
|
||||
$html[] = '</div></div>';
|
||||
}
|
||||
@@ -478,15 +543,8 @@ function auth_render_lager_open_lots(array $lagerProducts, string $manufacturerO
|
||||
$selectButton = '<button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-open-select data-lager-open-selectable="' . ($isQualified ? 'true' : 'false') . '"' . ($isQualified && empty($lot['next_active']) ? '' : ' disabled aria-disabled="true"') . '>Als nächste aktive auswählen</button>';
|
||||
$html[] = '<div class="sg-delete-confirmation-pattern__stage sg-delete-confirmation-pattern__host' . $selectedClass . '" data-pattern="overlay-card" data-dialog-open="false" data-lager-open-card-wrapper="' . $lotId . '"><article class="sg-card sg-card--content-card sg-delete-confirmation-pattern__target" data-component="content-card" data-pattern="overlay-card" data-shared-editable-action-card data-shared-editable-delete-token="DELETE" aria-label="Offene Charge">'
|
||||
. '<div 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">Offene Charge</div><div class="sg-component-row">' . $selectButton . '<button class="sg-interaction-element sg-button sg-button--warning" type="button" data-component="button" data-component-variant="warning" data-lager-open-delete data-shared-editable-card-delete>Löschen</button><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-open-edit-open data-shared-editable-card-edit>Bearbeiten</button></div></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-lager-open-card="' . (int) ($lot['lot_id'] ?? 0) . '" data-shared-editable-card-read><p class="sg-body"><strong>Auswahl:</strong> <span data-lager-open-card-next-active>' . auth_escape_html($selectedText) . '</span><br><strong>Naurua-Chargennummer:</strong> <span data-lager-open-card-lot-number>' . auth_escape_html((string) ($lot['lot_number'] ?? 'Nicht hinterlegt')) . '</span><br>'
|
||||
. '<strong>Status:</strong> Vorbereitet<br>'
|
||||
. '<strong>Bestand:</strong> <span data-lager-open-card-qty-net>' . auth_format_lager_quantity((float) ($lot['qty_net'] ?? 0)) . '</span><br>'
|
||||
. '<strong>Eingangsmenge:</strong> <span data-lager-open-card-qty-in>' . auth_format_lager_quantity((float) ($lot['qty_in'] ?? 0)) . '</span><br>'
|
||||
. '<strong>Hersteller:</strong> <span data-lager-open-card-manufacturer>' . auth_escape_html((string) ($lot['manufacturer_name'] ?? 'Nicht hinterlegt')) . '</span><br>'
|
||||
. '<strong>Hersteller-Chargennummer:</strong> <span data-lager-open-card-manufacturer-lot>' . auth_escape_html((string) ($lot['manufacturer_lot_number'] ?? 'Nicht hinterlegt')) . '</span><br>'
|
||||
. '<strong>Herstellungsdatum:</strong> <span data-lager-open-card-mfg-date>' . auth_format_lager_date($lot['mfg_date'] ?? null) . '</span><br>'
|
||||
. '<strong>MHD:</strong> <span data-lager-open-card-expiry-date>' . auth_format_lager_date($lot['expiry_date'] ?? null) . '</span><br>'
|
||||
. '<strong>Einkaufsdatum:</strong> <span data-lager-open-card-purchase-date>' . auth_format_lager_date($lot['purchase_date'] ?? null) . '</span></p></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-lager-open-card="' . (int) ($lot['lot_id'] ?? 0) . '" data-shared-editable-card-read><dl class="market-detail-grid"><div class="market-detail-row"><dt>Auswahl</dt><dd><span data-lager-open-card-next-active>' . auth_escape_html($selectedText) . '</span></dd></div><div class="market-detail-row"><dt>Naurua-Chargennummer</dt><dd><span data-lager-open-card-lot-number>' . auth_escape_html((string) ($lot['lot_number'] ?? 'Nicht hinterlegt')) . '</span></dd></div>'
|
||||
. '<div class="market-detail-row"><dt>Status</dt><dd>Vorbereitet</dd></div><div class="market-detail-row"><dt>Bestand</dt><dd><span data-lager-open-card-qty-net>' . auth_format_lager_quantity((float) ($lot['qty_net'] ?? 0)) . '</span></dd></div><div class="market-detail-row"><dt>Eingangsmenge</dt><dd><span data-lager-open-card-qty-in>' . auth_format_lager_quantity((float) ($lot['qty_in'] ?? 0)) . '</span></dd></div><div class="market-detail-row"><dt>Hersteller</dt><dd><span data-lager-open-card-manufacturer>' . auth_escape_html((string) ($lot['manufacturer_name'] ?? 'Nicht hinterlegt')) . '</span></dd></div><div class="market-detail-row"><dt>Hersteller-Chargennummer</dt><dd><span data-lager-open-card-manufacturer-lot>' . auth_escape_html((string) ($lot['manufacturer_lot_number'] ?? 'Nicht hinterlegt')) . '</span></dd></div><div class="market-detail-row"><dt>Herstellungsdatum</dt><dd><span data-lager-open-card-mfg-date>' . auth_format_lager_date($lot['mfg_date'] ?? null) . '</span></dd></div><div class="market-detail-row"><dt>MHD</dt><dd><span data-lager-open-card-expiry-date>' . auth_format_lager_date($lot['expiry_date'] ?? null) . '</span></dd></div><div class="market-detail-row"><dt>Einkaufsdatum</dt><dd><span data-lager-open-card-purchase-date>' . auth_format_lager_date($lot['purchase_date'] ?? null) . '</span></dd></div></dl></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-lager-open-edit-form data-shared-editable-card-form hidden><div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Offene Charge bearbeiten"><form class="sg-form-sections-card" data-lager-open-edit-form-element><div class="sg-form-sections-card__body" data-pattern-part="form-body"><h2 class="sg-strong sg-form-sections-card__title">Offene Charge bearbeiten</h2>'
|
||||
. '<section class="sg-form-sections-card__chapter" aria-labelledby="lager-open-charge-data-' . $lotId . '"><h2 id="lager-open-charge-data-' . $lotId . '" class="sg-strong sg-form-sections-card__chapter-title">Chargendaten</h2>'
|
||||
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Naurua-Chargennummer</span><input class="sg-interaction-element sg-input-single-line" type="text" data-lager-open-lot-number value="' . auth_escape_html((string) ($lot['lot_number'] ?? '')) . '"></label></div>'
|
||||
@@ -568,7 +626,7 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
|
||||
. '<div data-lager-drawer-topic="open-lots" aria-label="Offene Chargen">' . auth_render_lager_open_lots($lagerProducts, $manufacturerOptions) . '</div>'
|
||||
. '<article class="sg-card sg-card--content-card" data-component="content-card" data-lager-drawer-topic="lot" data-shared-editable-action-card aria-label="Aktive Charge">'
|
||||
. '<div 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">Aktive Charge</div><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-edit-open data-shared-editable-card-edit>Bearbeiten</button></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-lager-read-view data-shared-editable-card-read><p class="sg-body"><strong>Naurua-Chargennummer:</strong> <span data-lager-drawer-lot-number></span><br><strong>Aktueller Bestand:</strong> <span data-lager-drawer-current-qty></span><br><strong>Eingangsmenge:</strong> <span data-lager-drawer-qty-in></span><br><strong>Hersteller:</strong> <span data-lager-drawer-manufacturer-name></span><br><strong>Hersteller-Chargennummer:</strong> <span data-lager-drawer-manufacturer-lot-number></span><br><strong>Herstellungsdatum:</strong> <span data-lager-drawer-mfg-date></span><br><strong>MHD:</strong> <span data-lager-drawer-expiry-date></span><br><strong>Einkaufsdatum:</strong> <span data-lager-drawer-purchase-date></span></p></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-lager-read-view data-shared-editable-card-read><dl class="market-detail-grid"><div class="market-detail-row"><dt>Naurua-Chargennummer</dt><dd><span data-lager-drawer-lot-number></span></dd></div><div class="market-detail-row"><dt>Aktueller Bestand</dt><dd><span data-lager-drawer-current-qty></span></dd></div><div class="market-detail-row"><dt>Eingangsmenge</dt><dd><span data-lager-drawer-qty-in></span></dd></div><div class="market-detail-row"><dt>Hersteller</dt><dd><span data-lager-drawer-manufacturer-name></span></dd></div><div class="market-detail-row"><dt>Hersteller-Chargennummer</dt><dd><span data-lager-drawer-manufacturer-lot-number></span></dd></div><div class="market-detail-row"><dt>Herstellungsdatum</dt><dd><span data-lager-drawer-mfg-date></span></dd></div><div class="market-detail-row"><dt>MHD</dt><dd><span data-lager-drawer-expiry-date></span></dd></div><div class="market-detail-row"><dt>Einkaufsdatum</dt><dd><span data-lager-drawer-purchase-date></span></dd></div></dl></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-lager-edit-form data-shared-editable-card-form hidden><div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Aktive Charge bearbeiten">'
|
||||
. '<form class="sg-form-sections-card" data-lager-edit-form-element><div class="sg-form-sections-card__body" data-pattern-part="form-body"><h2 class="sg-strong sg-form-sections-card__title">Aktive Charge bearbeiten</h2>'
|
||||
. '<section class="sg-form-sections-card__chapter" aria-labelledby="lager-charge-traceability-title"><h2 id="lager-charge-traceability-title" class="sg-strong sg-form-sections-card__chapter-title">Rückverfolgbarkeit</h2>'
|
||||
@@ -596,7 +654,7 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
function render_auth_home_page(array $user, array $otcProducts = [], array $bestellungenTable = [], array $marketOccurrencesTable = [], array $lagerProducts = [], array $lagerArtikel = [], array $lagerManufacturers = []): void
|
||||
function render_auth_home_page(array $user, array $otcProducts = [], array $otcMarketOptions = [], array $bestellungenTable = [], array $marketOccurrencesTable = [], array $lagerProducts = [], array $lagerArtikel = [], array $lagerManufacturers = [], array $marketDefinitionsTable = []): void
|
||||
{
|
||||
$otcProductRows = '';
|
||||
foreach ($otcProducts as $product) {
|
||||
@@ -616,6 +674,26 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
$otcProductRows .= '</div>';
|
||||
}
|
||||
|
||||
$otcMarketOccurrenceOptions = [];
|
||||
$otcMarketDayOptions = '';
|
||||
foreach ($otcMarketOptions as $option) {
|
||||
$occurrenceId = (int) ($option['occurrence_id'] ?? 0);
|
||||
$marketDefinitionId = (int) ($option['market_definition_id'] ?? 0);
|
||||
$dayId = (int) ($option['day_id'] ?? 0);
|
||||
if ($occurrenceId <= 0 || $marketDefinitionId <= 0 || $dayId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$marketName = (string) ($option['market_name'] ?? '');
|
||||
$otcMarketOccurrenceOptions[$marketDefinitionId] = $marketName;
|
||||
$day = (string) ($option['date'] ?? '');
|
||||
$dayLabel = $day === '' ? '' : (new DateTimeImmutable($day))->format('d.m.Y');
|
||||
$otcMarketDayOptions .= '<option value="' . $dayId . '" data-market-definition-id="' . $marketDefinitionId . '" data-market-occurrence-id="' . $occurrenceId . '">' . auth_escape_html($dayLabel) . '</option>';
|
||||
}
|
||||
$otcMarketOccurrenceOptionHtml = '<option value="">Markt auswählen</option>';
|
||||
foreach ($otcMarketOccurrenceOptions as $marketDefinitionId => $marketName) {
|
||||
$otcMarketOccurrenceOptionHtml .= '<option value="' . $marketDefinitionId . '">' . auth_escape_html($marketName) . '</option>';
|
||||
}
|
||||
|
||||
if ($otcProductRows === '') {
|
||||
$otcProductRows = '<p class="sg-body sg-form-sections-card__sentence">Keine aktiven OTC-Produkte erfasst.</p>';
|
||||
}
|
||||
@@ -624,7 +702,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
[
|
||||
'Übersicht' => [],
|
||||
'ERP' => ['Bestellungen', 'Lager', 'Produkte', 'Artikel', 'Kontakte'],
|
||||
'Märkte' => ['Märkte', 'Konfiguration'],
|
||||
'Märkte' => ['Marktdurchführungen', 'Märkte'],
|
||||
'Buchhaltung' => [],
|
||||
'Kundenberatung' => [],
|
||||
],
|
||||
@@ -632,11 +710,13 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
);
|
||||
$bestellungenContent = auth_render_bestellungen_content($bestellungenTable);
|
||||
$marketsContent = auth_render_markets_content($marketOccurrencesTable);
|
||||
$marketDefinitionsContent = auth_render_market_definitions_content($marketDefinitionsTable);
|
||||
$lagerContent = auth_render_lager_content($lagerProducts, $lagerArtikel, $lagerManufacturers, auth_csrf_token());
|
||||
$moduleContentCards = json_encode(
|
||||
[
|
||||
'Bestellungen' => $bestellungenContent,
|
||||
'Märkte' => $marketsContent,
|
||||
'Marktdurchführungen' => $marketsContent,
|
||||
'Märkte' => $marketDefinitionsContent,
|
||||
'Lager' => $lagerContent,
|
||||
'Artikel' => auth_render_lager_content([], $lagerArtikel, $lagerManufacturers, auth_csrf_token(), 'Artikel'),
|
||||
],
|
||||
@@ -793,6 +873,32 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" id="otc-city" value="Oberengstringen" data-otc-order-city>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '</section>';
|
||||
echo '<section class="sg-form-sections-card__chapter" aria-labelledby="otc-market-link-title">';
|
||||
echo '<h2 id="otc-market-link-title" class="sg-strong sg-form-sections-card__chapter-title">Marktbezug</h2>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<label class="sg-checkbox-field-option sg-checkbox-field-option--inactive-selectable sg-body" data-component="checkbox-field" data-component-state="inactive-selectable">';
|
||||
echo '<button class="sg-checkbox-field sg-checkbox-field--inactive-selectable" type="button" role="checkbox" aria-checked="false" aria-label="Verkauf ist auf einem Markttag" data-otc-market-link-toggle>';
|
||||
echo '<span class="sg-checkbox-field__mark" aria-hidden="true">✓</span>';
|
||||
echo '</button><span>Verkauf ist auf einem Markttag</span>';
|
||||
echo '<input type="hidden" name="market_sale" value="0" data-otc-market-link-enabled>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<label class="sg-labeled-input-row"><span class="sg-label">Markt</span>';
|
||||
echo '<select class="sg-interaction-element sg-input-single-line sg-input-single-line--disabled sg-form-disabled sg-pulldown sg-pulldown--disabled" disabled aria-disabled="true" data-otc-market-occurrence>';
|
||||
echo $otcMarketOccurrenceOptionHtml;
|
||||
echo '</select>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<label class="sg-labeled-input-row"><span class="sg-label">Markttag</span>';
|
||||
echo '<select class="sg-interaction-element sg-input-single-line sg-input-single-line--disabled sg-form-disabled sg-pulldown sg-pulldown--disabled" disabled aria-disabled="true" data-otc-market-day>';
|
||||
echo '<option value="">Zuerst Markt auswählen</option>' . $otcMarketDayOptions;
|
||||
echo '</select>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '</section>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<div class="sg-body sg-otc-order-form__status hidden" data-otc-order-success>Bestellung erfolgreich erfasst! Die Bestellnummer wird automatisch generiert.</div>';
|
||||
echo '<div class="sg-body sg-otc-order-form__status sg-otc-order-form__status--error hidden" data-otc-order-error></div>';
|
||||
@@ -814,6 +920,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo '<div class="sg-delete-confirmation-pattern__list" data-otc-confirmation-products aria-label="Produkte"></div>';
|
||||
echo '<p class="sg-body sg-delete-confirmation-pattern__text"><strong>Gesamtzahl Flaschen: <span data-otc-confirmation-quantity>0</span></strong></p>';
|
||||
echo '<p class="sg-body sg-delete-confirmation-pattern__text"><strong>Gesamtbetrag: CHF <span data-otc-confirmation-total>0.00</span></strong></p>';
|
||||
echo '<p class="sg-body sg-otc-order-form__status sg-otc-order-form__status--error hidden" data-otc-confirmation-error aria-live="polite"></p>';
|
||||
echo '<div class="sg-delete-confirmation-pattern__actions">';
|
||||
echo '<button class="sg-interaction-element sg-button sg-button--active" type="button" data-otc-confirmation-cancel>Abbrechen</button>';
|
||||
echo '<button class="sg-interaction-element sg-button sg-button--process" type="button" data-otc-confirmation-submit>Verkaufen</button>';
|
||||
@@ -823,7 +930,8 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo '</div>';
|
||||
echo '</section>';
|
||||
echo '<script src="/scripts/help-icon-overlays.js"></script>';
|
||||
echo '<script src="/scripts/shared-editable-action-card.js"></script>';
|
||||
echo '<script src="/scripts/shared-editable-action-card.js?v=20260920-2"></script>';
|
||||
echo '<script src="/scripts/shared-mobile-detail.js?v=20260925-1"></script>';
|
||||
echo '<script>';
|
||||
echo 'const portalModuleNavigation = ' . $moduleNavigation . ';';
|
||||
echo 'const portalModuleContentCards = ' . $moduleContentCards . ';';
|
||||
@@ -832,19 +940,23 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const heading = document.querySelector('[data-main-heading]');";
|
||||
echo " if (heading && portalModuleHeadings[moduleName]) { heading.textContent = portalModuleHeadings[moduleName]; }";
|
||||
echo "};";
|
||||
echo "const renderLeftNavigation = (moduleName) => {";
|
||||
echo "const updatePortalLocation = (moduleName, entryName = '') => { const url = new URL(window.location.href); url.searchParams.set('portal_module', moduleName); if (entryName) { url.searchParams.set('portal_section', entryName); } else { url.searchParams.delete('portal_section'); } window.history.replaceState({ ...(window.history.state || {}), portalModule: moduleName, portalSection: entryName }, '', url); };";
|
||||
echo "const renderLeftNavigation = (moduleName, requestedEntry = '') => {";
|
||||
echo " const title = document.querySelector('[data-left-navigation-title]');";
|
||||
echo " const contentTitle = document.querySelector('[data-left-navigation-content-title]');";
|
||||
echo " const contentSection = document.querySelector('.sg-left-navigation-pattern__group-card--content');";
|
||||
echo " const contentBody = document.querySelector('[data-left-navigation-content-body]');";
|
||||
echo " const menu = document.getElementById('left-navigation-menu');";
|
||||
echo " if (title) { title.textContent = moduleName; }";
|
||||
echo " if (!menu) { return; }";
|
||||
echo " const entries = portalModuleNavigation[moduleName] || [];";
|
||||
echo " const activeEntry = entries[0] || '';";
|
||||
echo " const activeEntry = entries.includes(requestedEntry) ? requestedEntry : (entries[0] || '');";
|
||||
echo " if (contentTitle) { contentTitle.textContent = activeEntry; }";
|
||||
echo " if (contentSection) { contentSection.setAttribute('aria-hidden', 'false'); }";
|
||||
echo " if (contentBody) { contentBody.innerHTML = portalModuleContentCards[activeEntry] || ''; }";
|
||||
echo " if (activeEntry === 'Bestellungen') { initBestellungenBindings(); }";
|
||||
echo " if (activeEntry === 'Märkte') { initMarketsBindings(); }";
|
||||
echo " if (activeEntry === 'Marktdurchführungen') { initMarketsBindings(); }";
|
||||
echo " if (activeEntry === 'Märkte') { initMarketDefinitionsBindings(); }";
|
||||
echo " if (activeEntry === 'Lager' || activeEntry === 'Artikel') { initLagerBindings(); }";
|
||||
echo " menu.innerHTML = '';";
|
||||
echo " entries.forEach((entry, index) => {";
|
||||
@@ -853,7 +965,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " button.type = 'button';";
|
||||
echo " button.setAttribute('role', 'tab');";
|
||||
echo " button.setAttribute('data-component-part', 'tab-button');";
|
||||
echo " button.setAttribute('aria-selected', String(index === 0));";
|
||||
echo " button.setAttribute('aria-selected', String(entry === activeEntry));";
|
||||
echo " button.textContent = entry;";
|
||||
echo " menu.appendChild(button);";
|
||||
echo " });";
|
||||
@@ -862,12 +974,15 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " menu.querySelectorAll('.sg-tab-button').forEach((otherButton) => {";
|
||||
echo " otherButton.setAttribute('aria-selected', String(otherButton === button));";
|
||||
echo " });";
|
||||
echo " const entryName = button.textContent.trim();";
|
||||
echo " if (contentTitle) { contentTitle.textContent = entryName; }";
|
||||
echo " if (contentBody) { contentBody.innerHTML = portalModuleContentCards[entryName] || ''; }";
|
||||
echo " if (entryName === 'Bestellungen') { initBestellungenBindings(); }";
|
||||
echo " if (entryName === 'Märkte') { initMarketsBindings(); }";
|
||||
echo " if (entryName === 'Lager' || entryName === 'Artikel') { initLagerBindings(); }";
|
||||
echo " const entryName = button.textContent.trim();";
|
||||
echo " updatePortalLocation(moduleName, entryName);";
|
||||
echo " if (contentTitle) { contentTitle.textContent = entryName; }";
|
||||
echo " if (contentSection) { contentSection.setAttribute('aria-hidden', 'false'); }";
|
||||
echo " if (contentBody) { contentBody.innerHTML = portalModuleContentCards[entryName] || ''; }";
|
||||
echo " if (entryName === 'Bestellungen') { initBestellungenBindings(); }";
|
||||
echo " if (entryName === 'Marktdurchführungen') { initMarketsBindings(); }";
|
||||
echo " if (entryName === 'Märkte') { initMarketDefinitionsBindings(); }";
|
||||
echo " if (entryName === 'Lager' || entryName === 'Artikel') { initLagerBindings(); }";
|
||||
echo " });";
|
||||
echo " });";
|
||||
echo "};";
|
||||
@@ -879,8 +994,10 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " otherButton.setAttribute('aria-selected', String(isActive));";
|
||||
echo " otherButton.dataset.componentState = isActive ? 'active' : 'inactive';";
|
||||
echo " });";
|
||||
echo " renderMainHeading(button.textContent.trim());";
|
||||
echo " renderLeftNavigation(button.textContent.trim());";
|
||||
echo " const moduleName = button.textContent.trim();";
|
||||
echo " updatePortalLocation(moduleName);";
|
||||
echo " renderMainHeading(moduleName);";
|
||||
echo " renderLeftNavigation(moduleName);";
|
||||
echo " });";
|
||||
echo " });";
|
||||
echo "});";
|
||||
@@ -955,15 +1072,15 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " lagerBindingsInstalled = true;";
|
||||
echo " let lagerMobileDetailHost = null;";
|
||||
echo " let lagerMobileDetailOpen = false;";
|
||||
echo " const lagerMobileDetail = window.SharedMobileDetail?.create({ contentRoot, bodyClass: 'sg-lager-mobile-detail-open', hostClass: 'sg-lager-mobile-detail-host', hostDataAttribute: 'data-lager-mobile-detail-host', drawerDataAttribute: 'data-lager-mobile-drawer', backMarkup: '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\" aria-label=\"Navigation Detailansicht zurück oben\"><div class=\"sg-card-segment sg-card-segment--body\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-lager-detail-back>zurück</button></div></div></article></div></div>', historyKey: 'lagerView' });";
|
||||
echo " const renderLagerMobileDetail = (sourceDrawer, trigger) => { const type = trigger.dataset.lagerDrawerType || 'Lager'; const name = trigger.dataset.lagerDrawerName || ''; const value = trigger.dataset.lagerDrawerValue || ''; sourceDrawer.querySelector('[data-lager-drawer-title]').textContent = type + ': ' + name; sourceDrawer.querySelector('[data-lager-drawer-name-value]').textContent = name; sourceDrawer.querySelector('[data-lager-drawer-value-label]').textContent = type === 'Produkt' ? 'Anzahl im Lager:' : 'Artikelnummer:'; sourceDrawer.querySelector('[data-lager-drawer-value-value]').textContent = value; sourceDrawer.querySelectorAll('[data-lager-drawer-topic]').forEach((card) => { card.hidden = type !== 'Produkt'; }); const newLotForm = sourceDrawer.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); if (newLotForm) { newLotForm.hidden = true; } const newLotAction = sourceDrawer.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); if (newLotAction) { newLotAction.hidden = false; } if (type === 'Produkt') { const warning = trigger.dataset.lagerDrawerForecastWarning === 'true' || trigger.dataset.lagerDrawerForecastMhdWarning === 'true'; const warningValue = sourceDrawer.querySelector('[data-lager-drawer-warning-value]'); if (warningValue) { warningValue.textContent = warning ? (trigger.dataset.lagerDrawerForecast || 'Keine Prognose') : 'Keine akute Warnung.'; } const warningTitle = sourceDrawer.querySelector('[data-lager-drawer-warning-title]'); if (warningTitle) { warningTitle.textContent = getLagerWarningTitle(trigger); } sourceDrawer.querySelector('[data-lager-drawer-lot-number]').textContent = trigger.dataset.lagerDrawerLotNumber || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-current-qty]').textContent = trigger.dataset.lagerDrawerCurrentQty || '0'; sourceDrawer.querySelector('[data-lager-drawer-qty-in]').textContent = trigger.dataset.lagerDrawerQtyIn || '0'; sourceDrawer.querySelector('[data-lager-drawer-manufacturer-name]').textContent = trigger.dataset.lagerDrawerManufacturerName || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-manufacturer-lot-number]').textContent = trigger.dataset.lagerDrawerManufacturerLotNumber || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-mfg-date]').textContent = trigger.dataset.lagerDrawerMfgDate || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-expiry-date]').textContent = trigger.dataset.lagerDrawerExpiryDate || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-purchase-date]').textContent = trigger.dataset.lagerDrawerPurchaseDate || 'Nicht hinterlegt'; const productId = trigger.closest('[data-lager-row]')?.dataset.lagerId || ''; sourceDrawer.querySelectorAll('[data-lager-historical-product]').forEach((product) => { product.hidden = product.dataset.lagerHistoricalProduct !== productId; }); sourceDrawer.querySelectorAll('[data-lager-open-product]').forEach((product) => { product.hidden = product.dataset.lagerOpenProduct !== productId; product.querySelectorAll('[data-lager-open-edit-form]').forEach((form) => { form.hidden = true; }); product.querySelectorAll('[data-lager-open-card]').forEach((readView) => { readView.hidden = false; }); }); resetLagerNewLotConfirmation(); resetLagerOpenLotConfirmations(); sourceDrawer.dataset.lagerProductId = productId; populateLagerActiveLotForm(sourceDrawer, trigger); } const host = document.createElement('div'); host.className = 'sg-lager-mobile-detail-host'; host.dataset.lagerMobileDetailHost = 'true'; host.innerHTML = '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\" aria-label=\"Navigation Detailansicht zurück oben\"><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\" data-pattern-part=\"navigation-card-segment\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-lager-detail-back>zurück</button></div></div></article></div></div>'; const mobileDrawer = sourceDrawer; mobileDrawer.dataset.lagerMobileDrawer = 'true'; mobileDrawer.dataset.open = 'true'; mobileDrawer.setAttribute('aria-hidden', 'false'); host.appendChild(sourceDrawer); return host; };";
|
||||
echo " const restoreLagerMobileDetail = () => {";
|
||||
echo " if (lagerMobileDetailHost && lagerMobileDetailHost.parentNode) { const mobileDrawer = lagerMobileDetailHost.querySelector('[data-lager-mobile-drawer]'); if (mobileDrawer) { contentRoot.appendChild(mobileDrawer); mobileDrawer.removeAttribute('data-lager-mobile-drawer'); mobileDrawer.dataset.open = 'false'; mobileDrawer.setAttribute('aria-hidden', 'true'); } lagerMobileDetailHost.parentNode.removeChild(lagerMobileDetailHost); }";
|
||||
echo " lagerMobileDetailHost = null; lagerMobileDetailOpen = false; document.body.classList.remove('sg-lager-mobile-detail-open');";
|
||||
echo " lagerMobileDetail?.restore(); lagerMobileDetailHost = null; lagerMobileDetailOpen = false;";
|
||||
echo " };";
|
||||
echo " const openLagerMobileDetail = (trigger) => {";
|
||||
echo " restoreLagerMobileDetail(); const sourceDrawer = contentRoot.querySelector('[data-lager-drawer]'); lagerMobileDetailHost = sourceDrawer ? renderLagerMobileDetail(sourceDrawer, trigger) : null; const mobileDrawer = lagerMobileDetailHost?.querySelector('[data-lager-mobile-drawer]'); if (mobileDrawer) { const virtualLot = trigger.dataset.lagerDrawerVirtualLotNumber || ''; const virtualLotValue = mobileDrawer.querySelector('[data-lager-drawer-virtual-lot-value]'); if (virtualLotValue) { virtualLotValue.textContent = virtualLot ? 'Virtuelle Charge: ' + virtualLot : 'Keine virtuelle Fehlbestandscharge vorhanden.'; } }";
|
||||
echo " if (lagerMobileDetailHost) { document.body.appendChild(lagerMobileDetailHost); document.body.classList.add('sg-lager-mobile-detail-open'); }";
|
||||
echo " lagerMobileDetailOpen = true; window.history.pushState({ ...(window.history.state || {}), lagerView: 'detail' }, '', window.location.href);";
|
||||
echo " if (lagerMobileDetailHost && mobileDrawer) { lagerMobileDetail?.mount(mobileDrawer, trigger, lagerMobileDetailHost); if (window.SharedEditableActionCard) { window.SharedEditableActionCard.init(lagerMobileDetailHost); } }";
|
||||
echo " lagerMobileDetailOpen = true;";
|
||||
echo " };";
|
||||
echo " const closeLagerMobileDetail = () => {";
|
||||
echo " if (!lagerMobileDetailOpen) { return; }";
|
||||
@@ -1026,6 +1143,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo "function initMarketsBindings() {";
|
||||
echo " const contentRoot = document.querySelector('[data-left-navigation-content-body]');";
|
||||
echo " if (!contentRoot || !contentRoot.querySelector('[data-markets-large-table]')) { return; }";
|
||||
echo " if (window.SharedEditableActionCard) { window.SharedEditableActionCard.init(contentRoot); }";
|
||||
echo " const getTable = () => contentRoot.querySelector('[data-markets-large-table]');";
|
||||
echo " let searchTimer = null;";
|
||||
echo " const getSearchInput = () => getTable()?.querySelector('[data-markets-search-input]') || null;";
|
||||
@@ -1035,40 +1153,176 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const getCurrentLimit = () => parseInt(getTable()?.dataset.marketsLimit || '20', 10);";
|
||||
echo " const getPageSize = () => parseInt(getTable()?.dataset.marketsPageSize || '20', 10);";
|
||||
echo " const marketEscape = (value) => String(value ?? '').replace(/[&<>\"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[character] || character));";
|
||||
echo " const marketDisplay = (value) => value === null || value === undefined || value === '' ? 'Nicht hinterlegt' : marketEscape(value);";
|
||||
echo " const marketDisplay = (value) => { if (value === null || value === undefined || value === '') { return 'Nicht hinterlegt'; } const escaped = marketEscape(value); const safeMarkup = escaped.replace(/<br>/g, '<br>').replace(/<strong>/g, '<strong>').replace(/<\\/strong>/g, '</strong>').replace(/<a class="sg-hyperlink" href="(https?:\\/\\/.*?)" target="_blank" rel="noopener noreferrer">/g, '<a class=\"sg-hyperlink\" href=\"$1\" target=\"_blank\" rel=\"noopener noreferrer\">').replace(/<\\/a>/g, '</a>'); return safeMarkup.replace(/\\r?\\n/g, '<br>'); };";
|
||||
echo " const marketBoolean = (value) => value === true || value === 'true' || value === 1 || value === '1' ? 'Ja' : value === false || value === 'false' || value === 0 || value === '0' ? 'Nein' : 'Nicht hinterlegt';";
|
||||
echo " const marketMoney = (value) => value === null || value === undefined || value === '' ? 'Nicht hinterlegt' : 'CHF ' + Number(value).toFixed(2);";
|
||||
echo " const marketContactName = (contact) => { if (!contact) { return 'Nicht hinterlegt'; } return marketDisplay([contact.first_name, contact.last_name].filter(Boolean).join(' ')); };";
|
||||
echo " const marketDate = (value) => { if (!value) { return 'Nicht hinterlegt'; } const parts = String(value).split('-'); return parts.length === 3 ? parts[2] + '.' + parts[1] + '.' + parts[0] : marketEscape(value); };";
|
||||
echo " const marketStatusLabel = (value) => ({ open: 'Offen', waitlist: 'Warteliste', registered: 'Angemeldet', confirmation_pending: 'Bestätigung ausstehend', confirmed: 'Bestätigt', cancelled: 'Abgesagt', completed: 'Abgeschlossen' }[value] || value || 'Nicht hinterlegt');";
|
||||
echo " const 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--darkblue');";
|
||||
echo " const marketRow = (label, value, specific = false) => '<div class=\"market-detail-row\"><dt>' + marketEscape(label) + (specific ? ' <span class=\"sg-table-label market-day-specific\">(Tagesspezifisch)</span>' : '') + '</dt><dd>' + marketDisplay(value) + '</dd></div>';";
|
||||
echo " const marketCard = (title, body, headerClass = 'sg-card-segment--darkgreen', actions = '') => '<article class=\"sg-card sg-card--content-card market-detail-card\" data-component=\"content-card\"><header class=\"sg-card-segment sg-card-segment--header ' + headerClass + '\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">' + title + '</div>' + actions + '</header><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\">' + body + '</div></article>';";
|
||||
echo " const marketEditButton = (scope, id) => '<button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-market-edit=\"true\" data-market-edit-scope=\"' + scope + '\" data-market-edit-id=\"' + marketEscape(id) + '\">bearbeiten</button>';";
|
||||
echo " const renderMarketDetail = (payload) => { const market = payload.market || {}; const occurrence = payload.occurrence || {}; const days = Array.isArray(occurrence.days) ? occurrence.days : []; 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 overview = marketRow('Veranstaltung', 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); const dates = days.map((day) => marketDate(day.date)).join(' – '); 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); const infrastructure = marketRow('Standgrösse', occurrence.stand_size) + marketRow('Tisch vorhanden', occurrence.table_available) + marketRow('Tisch mietbar', occurrence.table_rentable) + marketRow('Eigener Tisch erforderlich', occurrence.own_table_required) + marketRow('Überdachung', occurrence.shelter_available) + marketRow('Strom', occurrence.power_available) + marketRow('Wasser', occurrence.water_available) + marketRow('Beleuchtung', occurrence.lighting_available); 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', occurrence.direct_unload_possible) + marketRow('Lagerung', occurrence.storage_options); const notes = marketRow('Teilnahmebedingungen', occurrence.other_participation_conditions) + marketRow('Rücktrittsbedingungen', occurrence.cancellation_terms) + marketRow('Umsatzbeteiligung', occurrence.revenue_share_terms) + marketRow('Notizen', occurrence.notes); const dayTabs = days.map((day, index) => '<button class=\"sg-interaction-element sg-button sg-tab-button\" type=\"button\" role=\"tab\" aria-selected=\"' + (index === 0 ? 'true' : 'false') + '\" aria-controls=\"market-day-panel-' + day.id + '\" id=\"market-day-tab-' + day.id + '\" data-market-day-tab=\"true\">' + marketDate(day.date) + '</button>').join(''); const dayPanels = days.map((day, index) => '<div class=\"sg-content-block-card-group__panel\" id=\"market-day-panel-' + day.id + '\" role=\"tabpanel\" aria-labelledby=\"market-day-tab-' + day.id + '\"' + (index === 0 ? '' : ' hidden') + '><div class=\"sg-content-cards-group\" data-pattern=\"content-cards-group\" aria-label=\"Durchführungstag ' + marketDate(day.date) + '\"><article class=\"sg-card sg-card--content-card\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\"><div class=\"sg-strong sg-text-on-dark\">' + marketDate(day.date) + '</div>' + marketEditButton('day', day.id) + '</header><div class=\"sg-card-segment sg-card-segment--body\"><dl class=\"market-detail-grid\">' + marketRow('Datum', marketDate(day.date), true) + marketRow('Standzeit von', day.stand_start_time, true) + marketRow('Standzeit bis', day.stand_end_time, true) + marketRow('Aufbau von', day.setup_start_time, true) + marketRow('Aufbau bis', day.setup_end_time, true) + marketRow('Anlieferung von', day.delivery_start_time, true) + marketRow('Anlieferung bis', day.delivery_end_time, true) + marketRow('Beobachtungen', day.observations, true) + '</dl></div></article></div></div>').join(''); return '<article class=\"sg-card sg-card--content-card market-status-card\"><header class=\"sg-card-segment sg-card-segment--header ' + marketStatusSegment(occurrence.status) + '\"><div class=\"sg-strong sg-text-on-dark\">Status: ' + marketEscape(marketStatusLabel(occurrence.status)) + '</div>' + marketEditButton('occurrence', occurrence.id) + '</header><div class=\"sg-card-segment sg-card-segment--body\"><dl class=\"market-detail-grid\">' + marketRow('Datumsbereich', dates) + marketRow('Anzahl Tage', days.length) + '</dl></div></article>' + marketCard('Veranstaltung und Durchführung', '<dl class=\"market-detail-grid\">' + overview + '</dl>', 'sg-card-segment--darkgreen') + marketCard('Teilnahme und Zahlung', '<dl class=\"market-detail-grid\">' + participation + '</dl>', 'sg-card-segment--darkgreen') + marketCard('Infrastruktur', '<dl class=\"market-detail-grid\">' + infrastructure + '</dl>', 'sg-card-segment--darkgreen') + marketCard('Anreise und Logistik', '<dl class=\"market-detail-grid\">' + travel + '</dl>', 'sg-card-segment--darkgreen') + marketC
Line truncated
|
||||
echo " const marketEditForm = (scope, id, values) => { const fields = scope === 'day' ? [['date', 'Datum', 'date'], ['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'], ['observations', 'Beobachtungen', 'text']] : [['place', 'Ort', 'text'], ['address', 'Adresse', 'text'], ['registration_deadline', 'Anmeldefrist', 'date'], ['request_or_registration_date', 'Anmeldung / Anfrage', 'date'], ['confirmation_date', 'Bestätigung', 'date'], ['payment_deadline', 'Zahlungsfrist', 'date'], ['notes', 'Notizen', 'text']]; return '<form class=\"market-edit-form\" data-market-edit-form=\"true\" data-market-edit-scope=\"' + scope + '\" data-market-edit-id=\"' + id + '\"><dl class=\"market-detail-grid\">' + fields.map(([key, label, type]) => '<div class=\"market-detail-row\"><dt>' + marketEscape(label) + '</dt><dd><input class=\"sg-interaction-element sg-input-single-line\" type=\"' + type + '\" name=\"' + key + '\" value=\"' + marketEscape(values[key] || '') + '\"></dd></div>').join('') + '</dl><div class=\"sg-component-row\"><button class=\"sg-interaction-element sg-button sg-button--process\" type=\"submit\">speichern</button><button class=\"sg-interaction-element sg-button sg-button--active\" type=\"button\" data-market-edit-cancel>abbrechen</button><span class=\"sg-body\" data-market-edit-status></span></div></form>'; };";
|
||||
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 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 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; };";
|
||||
echo " document.addEventListener('submit', (event) => { const form = event.target.closest('[data-market-edit-form]'); if (!form) { return; } ['table_available', 'table_rentable', 'own_table_required', 'shelter_available', 'power_available', 'water_available', 'lighting_available', 'direct_unload_possible'].forEach((field) => { const control = form.elements.namedItem(field); if (!control) { return; } const label = control.selectedOptions?.[0]?.textContent?.trim(); control.value = label === 'Ja' ? '1' : label === 'Nein' ? '0' : control.value === '' || label === 'Nicht hinterlegt' ? '' : control.value; }); }, true);";
|
||||
echo " const marketEditStatusObserver = new MutationObserver(() => { contentRoot.querySelectorAll('[data-market-edit-status], [data-market-create-status]').forEach((status) => { const message = status.textContent.trim(); if (!message || message === 'Speichern läuft …') { return; } const humanMessage = marketHumanizeError(new Error(message)); if (humanMessage !== message) { status.textContent = humanMessage; } status.classList.add('sg-form-validation-text'); status.setAttribute('role', 'alert'); }); }); marketEditStatusObserver.observe(contentRoot, { childList: true, characterData: true, subtree: true });";
|
||||
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 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.innerHTML; return Boolean(getTable()); };";
|
||||
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 " let marketMobileDetailHost = null; let marketOpeningMobileDetail = false; let marketDetailOpen = false;";
|
||||
echo " const marketMobileDetail = window.SharedMobileDetail?.create({ contentRoot, bodyClass: 'sg-market-mobile-detail-open', hostClass: 'sg-market-mobile-detail-host', hostDataAttribute: 'data-market-mobile-detail-host', drawerDataAttribute: 'data-market-mobile-drawer', backMarkup: '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\"><div class=\"sg-card-segment sg-card-segment--body\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-market-detail-back>zurück</button></div></div></article></div></div>', historyKey: 'marketView' });";
|
||||
echo " const getMarketDrawer = () => document.querySelector('[data-market-mobile-drawer]') || contentRoot.querySelector('[data-market-drawer]');";
|
||||
echo " const getMarketBackdrop = () => contentRoot.querySelector('[data-market-drawer-backdrop]');";
|
||||
echo " const setMarketDrawerState = (open) => { const drawer = getMarketDrawer(); const backdrop = getMarketBackdrop(); if (!drawer || !backdrop) { return; } drawer.dataset.open = String(open); drawer.setAttribute('aria-hidden', String(!open)); backdrop.dataset.open = String(open); backdrop.setAttribute('aria-hidden', String(!open)); document.body.classList.toggle('sg-card-list-page-drawer-open', open); };";
|
||||
echo " const applyMarketTabs = (root) => { root.querySelectorAll('[data-market-day-tab]').forEach((tab) => { if (tab.dataset.marketTabBound === 'true') { return; } tab.dataset.marketTabBound = 'true'; tab.addEventListener('click', () => { const group = tab.closest('[data-pattern=\"card-gruppe-mit-tastennavigation\"]'); group?.querySelectorAll('[data-market-day-tab]').forEach((candidate) => candidate.setAttribute('aria-selected', String(candidate === tab))); group?.querySelectorAll('[role=\"tabpanel\"]').forEach((panel) => { panel.hidden = panel.id !== tab.getAttribute('aria-controls'); }); tab.focus(); }); }); };";
|
||||
echo " const renderMarketEdit = (scope, id, payload) => { const drawer = getMarketDrawer(); const source = scope === 'day' ? (payload.occurrence.days || []).find((day) => String(day.id) === String(id)) : payload.occurrence; const card = drawer?.querySelector('[data-market-edit=\"true\"][data-market-edit-scope=\"' + scope + '\"][data-market-edit-id=\"' + id + '\"]')?.closest('article'); if (card && source) { card.querySelector('[data-component-part=\"card-body\"]')?.replaceChildren(); card.querySelector('[data-component-part=\"card-body\"]').innerHTML = marketEditForm(scope, id, source); } };";
|
||||
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 = payload.market?.name || 'Marktdurchführung'; } if (content) { content.innerHTML = renderMarketDetail(payload); applyMarketTabs(content); content.querySelectorAll('[data-market-edit]').forEach((button) => button.addEventListener('click', () => { const scope = button.dataset.marketEditScope || ''; const id = button.dataset.marketEditId || ''; const source = scope === 'day' ? (payload.occurrence.days || []).find((day) => String(day.id) === String(id)) : payload.occurrence; const card = button.closest('article'); const body = card?.querySelector('[data-component-part=\"card-body\"]'); if (body && source) { body.innerHTML = marketEditForm(scope, id, source); } })); } } 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 setMarketDrawerState = (open) => { const drawer = getMarketDrawer(); const backdrop = getMarketBackdrop(); if (!drawer || !backdrop) { return; } const drawerOpen = Boolean(open); const backdropOpen = drawerOpen && !marketDetailOpen; drawer.dataset.open = String(drawerOpen); drawer.setAttribute('aria-hidden', String(!drawerOpen)); backdrop.dataset.open = String(backdropOpen); backdrop.setAttribute('aria-hidden', String(!backdropOpen)); document.body.classList.toggle('sg-card-list-page-drawer-open', backdropOpen); };";
|
||||
echo " const applyMarketTabs = (root) => { const group = root.querySelector('[data-pattern=\"card-gruppe-mit-tastennavigation\"] [role=\"tablist\"]'); if (group) { group.dataset.component = 'tab-navigation'; group.dataset.componentSize = 'large'; group.querySelectorAll('[data-market-day-tab]').forEach((tab) => { tab.dataset.componentPart = 'tab-button'; tab.classList.remove('sg-button--active'); }); } root.querySelectorAll('[data-market-day-tab]').forEach((tab) => { if (tab.dataset.marketTabBound === 'true') { return; } tab.dataset.marketTabBound = 'true'; tab.addEventListener('click', () => { const tabGroup = tab.closest('[data-pattern=\"card-gruppe-mit-tastennavigation\"]'); tabGroup?.querySelectorAll('[data-market-day-tab]').forEach((candidate) => candidate.setAttribute('aria-selected', String(candidate === tab))); tabGroup?.querySelectorAll('[role=\"tabpanel\"]').forEach((panel) => { panel.hidden = panel.id !== tab.getAttribute('aria-controls'); }); tab.focus(); }); }); };";
|
||||
echo " const syncMarketDayAddButton = (root = contentRoot) => { if (!root.querySelector('[data-market-inherit-row]')) { enhanceMarketDayInheritance(root); } const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; const days = Array.isArray(payload?.occurrence?.days) ? payload.occurrence.days.filter((day) => !day.__draft).slice().sort((left, right) => String(left.date).localeCompare(String(right.date))) : []; const nextDate = (date) => { const value = new Date(String(date) + 'T00:00:00Z'); value.setUTCDate(value.getUTCDate() + 1); return value.toISOString().slice(0, 10); }; root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=\"day\"]').forEach((form) => { const input = form.querySelector('[name=\"date\"]'); if (!input) { return; } const index = days.findIndex((day) => String(day.id) === String(form.dataset.marketEditId)); const minimum = form.dataset.marketEditDraft === 'true' ? days.at(-1)?.date : index > 0 ? days[index - 1].date : ''; input.min = minimum ? nextDate(minimum) : ''; }); const addButton = root.querySelector('[data-market-day-add]'); if (!addButton) { return; } const editing = Array.from(root.querySelectorAll('[data-market-edit-form]')).some((form) => form.closest('[data-shared-editable-card-form]')?.hidden === false); addButton.disabled = editing; addButton.setAttribute('aria-disabled', String(editing)); addButton.classList.toggle('sg-button--active', !editing); addButton.classList.toggle('sg-button--inactive', editing); };";
|
||||
echo " const enhanceMarketDayInheritance = (root) => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; const days = Array.isArray(payload?.occurrence?.days) ? payload.occurrence.days.filter((day) => !day.__draft).slice().sort((left, right) => String(left.date).localeCompare(String(right.date))) : []; const firstDayId = days[0]?.id; root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=\"day\"]').forEach((form) => { const dayId = form.dataset.marketEditId; if (!dayId || String(dayId) === String(firstDayId)) { return; } const dateGroup = form.querySelector('[name=\"date\"]')?.closest('.sg-form-sections-card__field-group'); const observationsGroup = form.querySelector('[name=\"observations\"]')?.closest('.sg-form-sections-card__field-group'); if (!dateGroup || !observationsGroup) { return; } let row = form.querySelector('[data-market-inherit-row]'); if (!row) { row = document.createElement('div'); row.className = 'sg-form-sections-card__field-group'; row.dataset.marketInheritRow = 'true'; row.innerHTML = '<label class=\"sg-checkbox-field-option sg-body\" data-component=\"checkbox-field\" data-component-state=\"default\"><button class=\"sg-checkbox-field\" type=\"button\" role=\"checkbox\" aria-checked=\"false\" aria-label=\"Ab hier alle Werte aus Tag 1 übernehmen\" data-market-inherit-toggle><span class=\"sg-checkbox-field__mark\" aria-hidden=\"true\">✓</span></button><span>Ab hier alle Werte aus Tag 1 übernehmen</span><input type=\"hidden\" name=\"inherit_from_first_day\" value=\"0\"></label>'; observationsGroup.after(row); row.querySelector('[data-market-inherit-toggle]').addEventListener('click', (event) => { event.stopImmediatePropagation(); const toggle = row.querySelector('[data-market-inherit-toggle]'); const checked = toggle.getAttribute('aria-checked') !== 'true'; toggle.setAttribute('aria-checked', String(checked)); toggle.classList.toggle('sg-form-active', checked); row.querySelector('[name=\"inherit_from_first_day\"]').value = checked ? '1' : '0'; form.querySelectorAll('input, select, textarea').forEach((control) => { if (control.name !== 'date' && control.name !== 'inherit_from_first_day' && control.name !== 'observations') { control.disabled = checked; control.setAttribute('aria-disabled', String(checked)); control.classList.toggle('sg-form-disabled', checked); control.classList.toggle('sg-input-single-line--disabled', checked && control.tagName === 'INPUT'); } }); syncMarketDayAddButton(root); }); } const day = days.find((candidate) => String(candidate.id) === String(dayId)); const inherited = Boolean(day?.inherit_from_first_day); const toggle = row.querySelector('[data-market-inherit-toggle]'); toggle.setAttribute('aria-checked', String(inherited)); toggle.classList.toggle('sg-form-active', inherited); row.querySelector('[name=\"inherit_from_first_day\"]').value = inherited ? '1' : '0'; form.querySelectorAll('input, select, textarea').forEach((control) => { if (control.name !== 'date' && control.name !== 'inherit_from_first_day' && control.name !== 'observations') { control.disabled = inherited; control.setAttribute('aria-disabled', String(inherited)); control.classList.toggle('sg-form-disabled', inherited); control.classList.toggle('sg-input-single-line--disabled', inherited && control.tagName === 'INPUT'); } }); }); };";
|
||||
echo " const marketDayDeleteConfirmation = (id, label) => '<article class=\"sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card\" aria-label=\"Löschbestätigung für Markttag\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"market-day-delete-title-' + marketEscape(id) + '\" data-overlay-dialog=\"delete\" data-market-day-delete-confirmation 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-day-delete-title-' + marketEscape(id) + '\"><strong>Möchtest du den Markttag ' + marketEscape(label) + ' 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-day-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-day-delete-submit>Löschen</button></div></div></article>';";
|
||||
echo " const enhanceMarketDayDeleteControls = (root) => { const cards = Array.from(root.querySelectorAll('.market-days-group .sg-content-cards-group > [data-shared-editable-action-card]')); cards.forEach((card, index) => { if (index === 0 || card.querySelector('[data-market-edit-form][data-market-edit-draft=\"true\"]')) { return; } const form = card.querySelector('[data-market-edit-form][data-market-edit-scope=\"day\"]'); const actions = card.querySelector('.sg-object-card__header .sg-component-row'); if (!form || !actions || actions.querySelector('[data-market-day-delete-button]')) { return; } const dayId = form.dataset.marketEditId || ''; const label = card.querySelector('.sg-object-card__header .sg-strong')?.textContent?.trim() || 'diesen Markttag'; if (!dayId) { return; } const stage = document.createElement('div'); stage.className = 'sg-delete-confirmation-pattern__stage sg-delete-confirmation-pattern__host'; stage.dataset.pattern = 'overlay-card'; stage.dataset.dialogOpen = 'false'; card.classList.add('sg-delete-confirmation-pattern__target'); card.parentNode.replaceChild(stage, card); stage.appendChild(card); card.dataset.marketDayDeleteId = dayId; card.dataset.sharedEditableDeleteToken = 'DELETE'; actions.insertAdjacentHTML('afterbegin', '<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-day-delete-button>Löschen</button>'); card.insertAdjacentHTML('beforeend', marketDayDeleteConfirmation(dayId, label)); }); };";
|
||||
echo " const rerenderMarketDetail = (payload) => { const drawer = getMarketDrawer(); const content = drawer?.querySelector('[data-market-drawer-content]'); if (!drawer || !content) { return; } drawer.dataset.marketPayload = JSON.stringify(payload); content.innerHTML = renderMarketDetailWithActions(payload); applyMarketTabs(content); enhanceMarketDayDeleteControls(content); syncMarketDayAddButton(content); };";
|
||||
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 restoreMarketList = () => { if (marketMobileDetailHost?.parentNode) { const drawer = marketMobileDetailHost.querySelector('[data-market-mobile-drawer]'); if (drawer) { contentRoot.appendChild(drawer); drawer.removeAttribute('data-market-mobile-drawer'); drawer.dataset.open = 'false'; drawer.setAttribute('aria-hidden', 'true'); } marketMobileDetailHost.parentNode.removeChild(marketMobileDetailHost); } marketMobileDetailHost = null; marketDetailOpen = false; document.body.classList.remove('sg-market-mobile-detail-open'); bindTable(); };";
|
||||
echo " const openMarketMobileDetail = (trigger) => { restoreMarketList(); const sourceDrawer = contentRoot.querySelector('[data-market-drawer]'); if (!sourceDrawer) { return; } const host = document.createElement('div'); host.className = 'sg-market-mobile-detail-host'; host.dataset.marketMobileDetailHost = 'true'; host.innerHTML = '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\"><div class=\"sg-card-segment sg-card-segment--body\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-market-detail-back>zurück</button></div></div></article></div></div>'; sourceDrawer.dataset.marketMobileDrawer = 'true'; sourceDrawer.dataset.open = 'true'; sourceDrawer.setAttribute('aria-hidden', 'false'); host.appendChild(sourceDrawer); document.body.appendChild(host); marketMobileDetailHost = host; document.body.classList.add('sg-market-mobile-detail-open'); marketOpeningMobileDetail = true; void openMarketDrawer(trigger).finally(() => { marketOpeningMobileDetail = false; }); marketDetailOpen = true; window.history.pushState({ marketView: 'detail' }, '', window.location.href); };";
|
||||
echo " const 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 marketDrawerTitle = (payload) => { const marketName = payload.market?.name || 'Marktdurchführung'; const dates = (Array.isArray(payload.occurrence?.days) ? payload.occurrence.days : []).map((day) => marketDate(day.date)).filter(Boolean); return dates.length ? marketName + ', Marktdurchführung ' + dates.join(', ') : marketName; };";
|
||||
echo " const openMarketDrawer = async (trigger) => { const drawer = getMarketDrawer(); if (!drawer) { return; } const title = drawer.querySelector('[data-market-drawer-title]'); const content = drawer.querySelector('[data-market-drawer-content]'); setMarketDrawerState(true); if (title) { title.textContent = 'Marktdurchführung'; } if (content) { content.innerHTML = '<p class=\"sg-body\">Marktdetails werden geladen …</p>'; } try { const response = await fetch('/api/market-occurrence.php?id=' + encodeURIComponent(trigger.dataset.marketOccurrenceId || ''), { credentials: 'same-origin' }); const payload = await response.json(); if (!response.ok || !payload.ok) { throw new Error(payload.error || 'Marktdetails konnten nicht geladen werden.'); } drawer.dataset.marketPayload = JSON.stringify(payload); if (title) { title.textContent = marketDrawerTitle(payload); } if (content) { content.innerHTML = renderMarketDetailWithActions(payload); applyMarketTabs(content); syncMarketDayAddButton(content); } } catch (error) { if (content) { content.innerHTML = '<p class=\"sg-body\">' + marketEscape(error.message) + '</p>'; } } contentRoot.querySelectorAll('[data-market-drawer-open]').forEach((button) => button.setAttribute('aria-expanded', String(button === trigger))); };";
|
||||
echo " const openNewMarketDrawer = async () => { const drawer = getMarketDrawer(); if (!drawer) { return; } const title = drawer.querySelector('[data-market-drawer-title]'); const content = drawer.querySelector('[data-market-drawer-content]'); setMarketDrawerState(true); if (title) { title.textContent = 'Neue Marktdurchführung'; } if (content) { content.innerHTML = '<p class=\"sg-body\">Pflichtangaben werden geladen …</p>'; } try { const response = await fetch('/api/market-occurrence.php?action=create_options', { credentials: 'same-origin' }); const payload = await response.json(); if (!response.ok || !payload.ok) { throw new Error(payload.error || 'Pflichtangaben konnten nicht geladen werden.'); } if (content) { content.innerHTML = marketCreateForm(payload.market_definitions || []); } } catch (error) { if (content) { content.innerHTML = '<p class=\"sg-body\">' + marketEscape(error.message) + '</p>'; } } };";
|
||||
echo " const restoreMarketList = () => { marketMobileDetail?.restore(); marketMobileDetailHost = null; marketDetailOpen = false; bindTable(); };";
|
||||
echo " const openMarketMobileDetail = (trigger) => { restoreMarketList(); const sourceDrawer = contentRoot.querySelector('[data-market-drawer]'); if (!sourceDrawer) { return; } const host = document.createElement('div'); host.className = 'sg-market-mobile-detail-host'; host.dataset.marketMobileDetailHost = 'true'; host.innerHTML = '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\"><div class=\"sg-card-segment sg-card-segment--body\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-market-detail-back>zurück</button></div></div></article></div></div>'; sourceDrawer.dataset.marketMobileDrawer = 'true'; sourceDrawer.dataset.open = 'true'; sourceDrawer.setAttribute('aria-hidden', 'false'); host.appendChild(sourceDrawer); marketMobileDetailHost = host; marketDetailOpen = true; marketMobileDetail?.mount(sourceDrawer, trigger, host); if (window.SharedEditableActionCard) { window.SharedEditableActionCard.init(host); } marketOpeningMobileDetail = true; void openMarketDrawer(trigger).finally(() => { marketOpeningMobileDetail = false; }); };";
|
||||
echo " const deleteMarketOccurrence = async (submit) => { if (submit.disabled) { return; } const card = submit.closest('[data-shared-editable-action-card]'); const confirmation = Array.from(card?.children || []).find((child) => child.matches('[data-shared-editable-card-delete-confirmation]')); const input = confirmation?.querySelector('[data-shared-editable-card-delete-input]'); const status = confirmation?.querySelector('[data-market-delete-status]'); const occurrenceId = card?.dataset.marketDeleteOccurrenceId || ''; if (!card || !input || input.value !== 'DELETE' || !occurrenceId) { return; } if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'delete', id: occurrenceId, confirmation: input.value }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Löschen fehlgeschlagen.'); } closeMarketDrawer(); if (marketDetailOpen) { restoreMarketList(); } await loadFragment({ market_search: getSearchValue(), market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getCurrentLimit() }); } catch (error) { if (status) { status.textContent = marketEscape(error.message); } submit.disabled = false; submit.setAttribute('aria-disabled', 'false'); } };";
|
||||
echo " const deleteMarketDay = async (submit) => { if (submit.disabled) { return; } const card = submit.closest('[data-shared-editable-action-card]'); const input = card?.querySelector('[data-shared-editable-card-delete-input]'); const status = card?.querySelector('[data-market-day-delete-status]'); const dayId = card?.dataset.marketDayDeleteId || ''; if (!card || !input || input.value !== 'DELETE' || !dayId) { return; } if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'delete_day', id: dayId, confirmation: input.value }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Löschen fehlgeschlagen.'); } const trigger = contentRoot.querySelector('[data-market-drawer-open][aria-expanded=\"true\"]'); if (trigger) { await openMarketDrawer(trigger); } } catch (error) { if (status) { status.textContent = marketEscape(error.message); } submit.disabled = false; submit.setAttribute('aria-disabled', 'false'); } };";
|
||||
echo " const marketDayDeleteObserver = new MutationObserver(() => enhanceMarketDayDeleteControls(contentRoot)); marketDayDeleteObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
echo " const municipalityFieldObserver = new MutationObserver(() => initializeMunicipalityFields(contentRoot)); municipalityFieldObserver.observe(contentRoot, { childList: true, subtree: true }); initializeMunicipalityFields(contentRoot);";
|
||||
echo " const municipalityReadObserver = new MutationObserver(() => syncMunicipalityReadValue(contentRoot)); municipalityReadObserver.observe(contentRoot, { childList: true, subtree: true }); syncMunicipalityReadValue(contentRoot);";
|
||||
echo " if (!marketsBindingsInstalled) {";
|
||||
echo " marketsBindingsInstalled = true;";
|
||||
echo " document.addEventListener('click', (event) => { 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; } const edit = event.target.closest('[data-market-edit]'); if (edit) { const drawer = getMarketDrawer(); const current = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; if (current) { const scope = edit.dataset.marketEditScope || ''; const id = edit.dataset.marketEditId || ''; const source = scope === 'day' ? (current.occurrence.days || []).find((day) => String(day.id) === String(id)) : current.occurrence; const card = edit.closest('article'); const body = card?.querySelector('[data-component-part=\"card-body\"]'); if (body && source) { body.innerHTML = marketEditForm(scope, id, source); } } return; } if (event.target.closest('[data-market-edit-cancel]')) { const drawer = getMarketDrawer(); if (drawer?.dataset.marketPayload) { const payload = JSON.parse(drawer.dataset.marketPayload); const content = drawer.querySelector('[data-market-drawer-content]'); content.innerHTML = renderMarketDetail(payload); applyMarketTabs(content); } } });";
|
||||
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()); 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: form.dataset.marketEditId, 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 dayAdd = event.target.closest('[data-market-day-add]'); if (dayAdd) { event.preventDefault(); addMarketDayDraft(); return; } const editButton = event.target.closest('[data-shared-editable-card-edit]'); if (editButton) { window.setTimeout(() => syncMarketDayAddButton(contentRoot), 0); return; } const cancelButton = event.target.closest('[data-shared-editable-card-cancel]'); if (cancelButton) { if (cancelButton.closest('[data-market-edit-form][data-market-edit-draft=\"true\"]')) { window.setTimeout(discardMarketDayDraft, 0); } else { window.setTimeout(() => syncMarketDayAddButton(contentRoot), 0); } } });";
|
||||
echo " document.addEventListener('click', (event) => { const deleteDaySubmit = event.target.closest('[data-market-day-delete-submit]'); if (deleteDaySubmit) { void deleteMarketDay(deleteDaySubmit); return; } const createButton = event.target.closest('[data-market-occurrence-create]'); if (createButton) { event.preventDefault(); void openNewMarketDrawer(); return; } if (event.target.closest('[data-market-create-cancel]')) { closeMarketDrawer(); } });";
|
||||
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('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(); } });";
|
||||
echo " }";
|
||||
echo " const normalizeOccurrenceOverviewForm = (root) => { root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=overview]').forEach((form) => { if (form.dataset.marketOccurrenceFieldsNormalized === 'true') { return; } const organizerSelect = form.querySelector('[name=organization_contact_id]'); const contactSelect = form.querySelector('[name=contact_person_id]'); const organizerField = organizerSelect?.closest('.sg-form-sections-card__field-group'); const contactField = contactSelect?.closest('.sg-form-sections-card__field-group'); if (organizerField && organizerSelect) { const value = organizerSelect.selectedOptions[0]?.textContent || 'Nicht hinterlegt'; organizerField.innerHTML = '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Veranstalter</span><input class=\"sg-interaction-element sg-input-single-line sg-input-single-line--disabled sg-form-disabled\" type=\"text\" name=\"organizer_name\" value=\"' + marketEscape(value) + '\" disabled aria-disabled=\"true\"></label>'; } if (contactField && contactSelect) { const value = contactSelect.selectedOptions[0]?.textContent || ''; contactField.innerHTML = '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Kontaktperson</span><input class=\"sg-interaction-element sg-input-single-line\" type=\"text\" name=\"contact_person_name\" value=\"' + marketEscape(value) + '\"></label>'; } form.dataset.marketOccurrenceFieldsNormalized = 'true'; }); };";
|
||||
echo " const marketOccurrenceFieldObserver = new MutationObserver(() => normalizeOccurrenceOverviewForm(contentRoot)); marketOccurrenceFieldObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
echo " const normalizeMarketSimpleCostFields = (root) => { root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation]').forEach((form) => { if (form.dataset.marketSimpleCostsNormalized === 'true') { return; } const drawer = getMarketDrawer(); let payload = {}; try { payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : {}; } catch (error) {} const item = Array.isArray(payload.occurrence?.cost_items) ? payload.occurrence.cost_items[0] : null; const total = form.querySelector('[name=gross_cost]'); const text = form.querySelector('[name=cost_items_text]'); const totalLabel = total?.closest('.sg-labeled-input-row')?.querySelector('.sg-label'); const textLabel = text?.closest('.sg-labeled-input-row')?.querySelector('.sg-label'); if (total) { total.disabled = false; total.removeAttribute('disabled'); total.type = 'number'; total.step = '0.01'; total.min = '0'; total.required = true; total.classList.remove('sg-input-single-line--disabled', 'sg-form-disabled'); total.value = payload.occurrence?.cost_summary?.gross_amount ?? item?.gross_amount ?? ''; } if (text) { text.disabled = false; text.removeAttribute('disabled'); text.classList.remove('sg-form-disabled'); text.value = payload.occurrence?.cost_items_text ?? item?.description ?? ''; } if (totalLabel) { totalLabel.textContent = 'Gesamtkosten'; } if (textLabel) { textLabel.textContent = 'Kostenpositionen'; } form.dataset.marketSimpleCostsNormalized = 'true'; }); };";
|
||||
echo " const marketSimpleCostFieldsObserver = new MutationObserver(() => normalizeMarketSimpleCostFields(contentRoot)); marketSimpleCostFieldsObserver.observe(contentRoot, { childList: true, subtree: true }); normalizeMarketSimpleCostFields(contentRoot);";
|
||||
echo " const normalizeMarketTaxFields = (root) => { root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation]').forEach((form) => { if (form.dataset.marketTaxFieldsNormalized === 'true') { return; } const total = form.querySelector('[name=gross_cost]'); const totalGroup = total?.closest('.sg-form-sections-card__field-group'); if (!totalGroup) { return; } const drawer = getMarketDrawer(); let payload = {}; try { payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : {}; } catch (error) {} const item = Array.isArray(payload.occurrence?.cost_items) ? payload.occurrence.cost_items[0] : null; const taxStatusGroup = document.createElement('div'); taxStatusGroup.className = 'sg-form-sections-card__field-group'; taxStatusGroup.innerHTML = marketEditField('tax_status', 'Steuerstatus', 'select', item?.tax_status || 'taxable', [['taxable', 'Steuerpflichtig'], ['tax_free', 'Steuerfrei']], true); const taxRateGroup = document.createElement('div'); taxRateGroup.className = 'sg-form-sections-card__field-group'; taxRateGroup.innerHTML = marketEditField('tax_rate', 'Steuersatz', 'number', item?.tax_rate ?? '8.10', [], true); totalGroup.after(taxStatusGroup, taxRateGroup); const taxStatus = taxStatusGroup.querySelector('[name=tax_status]'); const taxRate = taxRateGroup.querySelector('[name=tax_rate]'); const sync = () => { const taxFree = taxStatus?.value === 'tax_free'; if (taxFree && taxRate) { taxRate.value = '0.00'; } if (taxRate) { taxRate.disabled = taxFree; taxRate.setAttribute('aria-disabled', String(taxFree)); } }; taxStatus?.addEventListener('change', sync); sync(); form.dataset.marketTaxFieldsNormalized = 'true'; }); };";
|
||||
echo " const marketTaxFieldsObserver = new MutationObserver(() => normalizeMarketTaxFields(contentRoot)); marketTaxFieldsObserver.observe(contentRoot, { childList: true, subtree: true }); normalizeMarketTaxFields(contentRoot);";
|
||||
echo " const marketTaxRateAttributesObserver = new MutationObserver(() => { contentRoot.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation] [name=tax_rate]').forEach((input) => { input.step = '0.01'; input.min = '0'; }); }); marketTaxRateAttributesObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
echo " const marketPaidAmountObserver = new MutationObserver(() => { contentRoot.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation] [name=paid_amount]').forEach((input) => { input.disabled = false; input.removeAttribute('disabled'); input.type = 'number'; input.step = '0.01'; input.min = '0'; input.classList.remove('sg-input-single-line--disabled', 'sg-form-disabled'); }); }); marketPaidAmountObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
echo " const marketPaidAmountPositionObserver = new MutationObserver(() => { contentRoot.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation]').forEach((form) => { const paidGroup = form.querySelector('[name=paid_amount]')?.closest('.sg-form-sections-card__field-group'); const taxGroup = form.querySelector('[name=tax_cost]')?.closest('.sg-form-sections-card__field-group'); if (paidGroup && taxGroup && taxGroup.nextElementSibling !== paidGroup) { taxGroup.after(paidGroup); } }); }); marketPaidAmountPositionObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
echo " const syncMarketSimpleCostTotals = (form) => { const gross = Number.parseFloat(form.querySelector('[name=gross_cost]')?.value || '0'); const rate = form.querySelector('[name=tax_status]')?.value === 'tax_free' ? 0 : Number.parseFloat(form.querySelector('[name=tax_rate]')?.value || '0'); const tax = Number.isFinite(gross) && Number.isFinite(rate) ? Math.round((gross - gross / (1 + rate / 100)) * 100) / 100 : 0; const net = Number.isFinite(gross) ? Math.round((gross - tax) * 100) / 100 : 0; const format = (value) => 'CHF ' + value.toFixed(2); const netField = form.querySelector('[name=net_cost]'); const taxField = form.querySelector('[name=tax_cost]'); if (netField) { netField.value = format(net); } if (taxField) { taxField.value = format(tax); } };";
|
||||
echo " document.addEventListener('input', (event) => { const field = event.target.closest('[data-market-edit-form][data-market-edit-scope=participation] [name=gross_cost], [data-market-edit-form][data-market-edit-scope=participation] [name=tax_rate]'); if (field) { syncMarketSimpleCostTotals(field.closest('[data-market-edit-form]')); } });";
|
||||
echo " document.addEventListener('change', (event) => { const field = event.target.closest('[data-market-edit-form][data-market-edit-scope=participation] [name=tax_status]'); if (field) { syncMarketSimpleCostTotals(field.closest('[data-market-edit-form]')); } });";
|
||||
echo " bindTable();";
|
||||
echo "}";
|
||||
echo "function initMarketDefinitionsBindings() {";
|
||||
echo " const contentRoot = document.querySelector('[data-left-navigation-content-body]');";
|
||||
echo " const definitionContent = contentRoot?.querySelector('[data-market-definitions-content]');";
|
||||
echo " const drawer = definitionContent?.querySelector('[data-market-definition-drawer]');";
|
||||
echo " const readCard = drawer?.querySelector('[data-market-definition-read-card]');";
|
||||
echo " const card = drawer?.querySelector('[data-market-definition-create-card]');";
|
||||
echo " const form = drawer?.querySelector('[data-market-definition-form]');";
|
||||
echo " const editButton = drawer?.querySelector('[data-market-definition-edit]');";
|
||||
echo " if (!contentRoot || !definitionContent || !drawer || !readCard || !card || !form || !editButton || definitionContent.dataset.marketDefinitionsBound === 'true') { return; }";
|
||||
echo " definitionContent.dataset.marketDefinitionsBound = 'true';";
|
||||
echo " let activeMarket = null;";
|
||||
echo " let marketDefinitionMobileHost = null; let marketDefinitionMobileOpen = false;";
|
||||
echo " const marketDefinitionMobileDetail = window.SharedMobileDetail?.create({ contentRoot, bodyClass: 'sg-market-definition-mobile-detail-open', hostClass: 'sg-market-definition-mobile-detail-host', hostDataAttribute: 'data-market-definition-mobile-detail-host', drawerDataAttribute: 'data-market-definition-mobile-drawer', backMarkup: '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\"><div class=\"sg-card-segment sg-card-segment--body\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-market-definition-detail-back>zurück</button></div></div></article></div></div>', historyKey: 'marketDefinitionView' });";
|
||||
echo <<<JS
|
||||
const prepareMarketDefinitionDeleteCard = () => {
|
||||
const existingCard = drawer.querySelector('[data-market-definition-card]');
|
||||
if (!existingCard || drawer.querySelector('[data-market-definition-delete-id]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stage = document.createElement('div');
|
||||
stage.className = 'sg-delete-confirmation-pattern__stage sg-delete-confirmation-pattern__host';
|
||||
stage.dataset.pattern = 'overlay-card';
|
||||
stage.dataset.dialogOpen = 'false';
|
||||
const target = document.createElement('div');
|
||||
target.className = 'sg-delete-confirmation-pattern__target';
|
||||
target.dataset.sharedEditableActionCard = '';
|
||||
target.dataset.sharedEditableDeleteToken = 'DELETE';
|
||||
target.dataset.marketDefinitionDeleteId = '';
|
||||
target.dataset.marketDefinitionCard = '';
|
||||
|
||||
const titleCard = document.createElement('article');
|
||||
titleCard.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.dataset.marketDefinitionReadTitle = '';
|
||||
heading.dataset.marketDefinitionDrawerTitle = '';
|
||||
heading.textContent = 'Markt';
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'sg-component-row';
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.className = 'sg-interaction-element sg-button sg-button--warning';
|
||||
deleteButton.type = 'button';
|
||||
deleteButton.dataset.sharedEditableCardDelete = '';
|
||||
deleteButton.dataset.marketDefinitionDelete = '';
|
||||
deleteButton.textContent = 'Löschen';
|
||||
actions.append(deleteButton);
|
||||
header.append(heading, actions);
|
||||
titleCard.append(header);
|
||||
existingCard.querySelector('[data-market-definition-read-title]')?.remove();
|
||||
existingCard.removeAttribute('data-shared-editable-action-card');
|
||||
|
||||
const confirmation = document.createElement('article');
|
||||
confirmation.className = 'sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card';
|
||||
confirmation.setAttribute('aria-label', 'Löschbestätigung');
|
||||
confirmation.setAttribute('role', 'dialog');
|
||||
confirmation.setAttribute('aria-modal', 'true');
|
||||
confirmation.dataset.overlayDialog = 'delete';
|
||||
confirmation.dataset.sharedEditableCardDeleteConfirmation = '';
|
||||
confirmation.hidden = true;
|
||||
confirmation.innerHTML = '<div class="sg-card-segment sg-card-segment--body sg-delete-confirmation-pattern__body"><p class="sg-body sg-delete-confirmation-pattern__text"><strong>Möchtest du diesen Markt 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-definition-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-definition-delete-submit>Löschen</button></div></div>';
|
||||
target.append(titleCard, existingCard, confirmation);
|
||||
stage.append(target);
|
||||
drawer.querySelector('[data-market-definition-drawer-content]')?.append(stage);
|
||||
};
|
||||
JS;
|
||||
echo " prepareMarketDefinitionDeleteCard();";
|
||||
echo " const sharedCard = drawer.querySelector('[data-market-definition-card]');";
|
||||
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; };";
|
||||
echo " const openMarketDefinitionMobileDetail = (trigger) => { restoreMarketDefinitionMobileDetail(); const sourceDrawer = drawer; const host = document.createElement('div'); host.className = 'sg-market-definition-mobile-detail-host'; host.dataset.marketDefinitionMobileDetailHost = 'true'; sourceDrawer.dataset.marketDefinitionMobileDrawer = 'true'; sourceDrawer.dataset.open = 'true'; sourceDrawer.setAttribute('aria-hidden', 'false'); host.appendChild(sourceDrawer); marketDefinitionMobileHost = host; marketDefinitionMobileOpen = true; marketDefinitionMobileDetail?.mount(sourceDrawer, trigger, host); if (window.SharedEditableActionCard) { window.SharedEditableActionCard.init(host); } showReadCard(trigger); };";
|
||||
echo " const resetForm = () => { form.reset(); const status = form.querySelector('[data-market-definition-status]'); if (status) { status.textContent = ''; } syncNewOrganizerFields(); };";
|
||||
echo " const syncNewOrganizerFields = () => { const select = form.querySelector('[data-market-definition-organizer]'); const fields = form.querySelector('[data-market-definition-new-organizer]'); if (!select || !fields) { return; } fields.hidden = select.value !== '__new__'; };";
|
||||
echo " const closeDrawer = () => { resetForm(); if (sharedCard && window.SharedEditableActionCard) { window.SharedEditableActionCard.resetDeleteConfirmation(sharedCard); window.SharedEditableActionCard.setEditMode(sharedCard, false); } activeMarket = null; setDrawerState(false); };";
|
||||
echo " const deleteMarketDefinition = async (submit) => { if (submit.disabled || !activeMarket?.id) { return; } const card = submit.closest('[data-shared-editable-action-card]'); const input = card?.querySelector('[data-shared-editable-card-delete-input]'); const status = card?.querySelector('[data-market-definition-delete-status]'); if (!card || !input || input.value !== 'DELETE') { return; } if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); try { const response = await fetch('/api/market-definition.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': definitionContent.dataset.marketDefinitionsCsrfToken || '' }, body: JSON.stringify({ action: 'delete', id: activeMarket.id, confirmation: input.value }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Löschen fehlgeschlagen.'); } const row = activeMarket.trigger.closest('[data-market-definition-row]'); row?.remove(); closeDrawer(); } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Löschen fehlgeschlagen.'; } submit.disabled = false; submit.setAttribute('aria-disabled', 'false'); } };";
|
||||
echo " const showReadCard = (trigger) => { activeMarket = { id: trigger.dataset.marketDefinitionId || '', trigger }; drawer.querySelector('[data-market-definition-read-title]').textContent = trigger.dataset.marketDefinitionName || 'Markt'; readCard.querySelector('[data-market-definition-read-name]').textContent = trigger.dataset.marketDefinitionName || 'Nicht hinterlegt'; readCard.querySelector('[data-market-definition-read-category]').textContent = trigger.dataset.marketDefinitionCategoryName || 'Nicht hinterlegt'; readCard.querySelector('[data-market-definition-read-organizer]').textContent = trigger.dataset.marketDefinitionOrganizerName || 'Nicht hinterlegt'; readCard.querySelector('[data-market-definition-read-website]').textContent = trigger.dataset.marketDefinitionWebsite || 'Nicht hinterlegt'; const title = form.querySelector('[data-market-definition-form-title]'); if (title) { title.textContent = 'Markt bearbeiten'; } const name = form.querySelector('[data-market-definition-name]'); const category = form.querySelector('[data-market-definition-category]'); const organizer = form.querySelector('[data-market-definition-organizer]'); const website = form.querySelector('[data-market-definition-website]'); if (name) { name.value = trigger.dataset.marketDefinitionName || ''; } if (category) { category.value = trigger.dataset.marketDefinitionCategoryId || ''; } if (organizer) { organizer.value = trigger.dataset.marketDefinitionOrganizerId || ''; } if (website) { website.value = trigger.dataset.marketDefinitionWebsite === 'Nicht hinterlegt' ? '' : (trigger.dataset.marketDefinitionWebsite || ''); } syncNewOrganizerFields(); if (sharedCard && window.SharedEditableActionCard) { window.SharedEditableActionCard.setEditMode(sharedCard, false); } setDrawerState(true); };";
|
||||
echo " definitionContent.addEventListener('click', (event) => { const trigger = event.target.closest('[data-market-definition-open]'); if (!trigger || !definitionContent.contains(trigger)) { return; } event.preventDefault(); event.stopPropagation(); if (window.matchMedia('(max-width: 767px)').matches) { openMarketDefinitionMobileDetail(trigger); } else { showReadCard(trigger); } }, true);";
|
||||
echo " definitionContent.addEventListener('click', (event) => { const deleteSubmit = event.target.closest('[data-market-definition-delete-submit]'); if (deleteSubmit) { void deleteMarketDefinition(deleteSubmit); return; } const trigger = event.target.closest('[data-market-definition-open]'); if (trigger) { const deleteButton = drawer.querySelector('[data-market-definition-delete]'); if (deleteButton) { deleteButton.hidden = false; } } if (event.target.closest('[data-market-definition-create]')) { const deleteButton = drawer.querySelector('[data-market-definition-delete]'); if (deleteButton) { deleteButton.hidden = true; } } });";
|
||||
echo " const openEditCard = () => { if (!activeMarket) { return; } const title = form.querySelector('[data-market-definition-form-title]'); if (title) { title.textContent = 'Markt bearbeiten'; } const name = form.querySelector('[data-market-definition-name]'); const category = form.querySelector('[data-market-definition-category]'); const organizer = form.querySelector('[data-market-definition-organizer]'); const website = form.querySelector('[data-market-definition-website]'); if (name) { name.value = activeMarket.trigger.dataset.marketDefinitionName || ''; } if (category) { category.value = activeMarket.trigger.dataset.marketDefinitionCategoryId || ''; } if (organizer) { organizer.value = activeMarket.trigger.dataset.marketDefinitionOrganizerId || ''; } if (website) { website.value = activeMarket.trigger.dataset.marketDefinitionWebsite === 'Nicht hinterlegt' ? '' : (activeMarket.trigger.dataset.marketDefinitionWebsite || ''); } syncNewOrganizerFields(); };";
|
||||
echo " editButton.addEventListener('click', (event) => { event.preventDefault(); openEditCard(); });";
|
||||
echo " form.querySelector('[data-market-definition-organizer]')?.addEventListener('change', syncNewOrganizerFields);";
|
||||
echo " const appendMarketRow = (market) => { const table = contentRoot.querySelector('[data-market-definitions-table]'); if (!table) { return; } const row = document.createElement('div'); row.className = 'sg-card-segment sg-card-segment--body sg-large-table__row'; row.setAttribute('role', 'row'); row.dataset.componentPart = 'large-table-row'; row.dataset.marketDefinitionRow = ''; row.dataset.marketDefinitionId = String(market.id || ''); const category = form.querySelector('[data-market-definition-category]')?.selectedOptions[0]?.textContent || ''; const organizer = form.querySelector('[data-market-definition-organizer]')?.selectedOptions[0]?.textContent || ''; const website = market.website_url || 'Nicht hinterlegt'; const cells = document.createElement('div'); cells.className = 'sg-large-table__cell'; const trigger = document.createElement('a'); trigger.className = 'sg-hyperlink'; trigger.href = '#'; trigger.setAttribute('aria-haspopup', 'dialog'); trigger.setAttribute('aria-expanded', 'false'); trigger.dataset.marketDefinitionOpen = ''; trigger.dataset.marketDefinitionId = String(market.id || ''); trigger.dataset.marketDefinitionName = market.name || ''; trigger.dataset.marketDefinitionCategoryId = String(market.category_id || ''); trigger.dataset.marketDefinitionCategoryName = category; trigger.dataset.marketDefinitionOrganizerId = String(market.organizer_id || ''); trigger.dataset.marketDefinitionOrganizerName = organizer; trigger.dataset.marketDefinitionWebsite = website; trigger.textContent = market.name || ''; cells.appendChild(trigger); row.appendChild(cells); [category, organizer, website, '0', 'Aktiv'].forEach((value) => { const cell = document.createElement('div'); cell.className = 'sg-large-table__cell'; cell.setAttribute('role', 'cell'); cell.textContent = value; row.appendChild(cell); }); table.appendChild(row); };";
|
||||
echo " document.addEventListener('click', (event) => { if (event.target.closest('[data-market-definition-create]')) { event.preventDefault(); activeMarket = null; resetForm(); const title = form.querySelector('[data-market-definition-form-title]'); if (title) { title.textContent = 'Neuen Markt anlegen'; } if (sharedCard && window.SharedEditableActionCard) { window.SharedEditableActionCard.setEditMode(sharedCard, true); } setDrawerState(true); form.querySelector('[data-market-definition-name]')?.focus(); return; } if (event.target.closest('[data-market-definition-edit]')) { event.preventDefault(); openEditCard(); return; } if (event.target.closest('[data-market-definition-cancel]')) { event.preventDefault(); if (!activeMarket) { closeDrawer(); } return; } if (event.target.closest('[data-market-definition-drawer-backdrop]')) { event.preventDefault(); closeDrawer(); } });";
|
||||
echo " document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && drawer.dataset.open === 'true') { closeDrawer(); } });";
|
||||
echo " document.addEventListener('click', (event) => { if (event.target.closest('[data-market-definition-detail-back]')) { event.preventDefault(); if (window.history.state?.marketDefinitionView === 'detail') { window.history.back(); } else { restoreMarketDefinitionMobileDetail(); } } });";
|
||||
echo " window.addEventListener('popstate', (event) => { if (marketDefinitionMobileOpen && (!event.state || event.state.marketDefinitionView !== 'detail')) { restoreMarketDefinitionMobileDetail(); } });";
|
||||
echo " form.addEventListener('submit', async (event) => { event.preventDefault(); const status = form.querySelector('[data-market-definition-status]'); const submit = form.querySelector('[type=submit]'); const fields = Object.fromEntries(new FormData(form).entries()); const isUpdate = Boolean(activeMarket?.id); if (fields.organizer_id === '__new__' && !String(fields.organizer_name || '').trim()) { if (status) { status.textContent = 'Name des neuen Veranstalters ist erforderlich.'; } return; } if (status) { status.textContent = 'Speichern läuft …'; } if (submit) { submit.disabled = true; } try { const response = await fetch('/api/market-definition.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': definitionContent.dataset.marketDefinitionsCsrfToken || '' }, body: JSON.stringify({ action: isUpdate ? 'update' : 'create', id: isUpdate ? activeMarket.id : null, fields }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } if (isUpdate) { const trigger = activeMarket.trigger; const category = form.querySelector('[data-market-definition-category]')?.selectedOptions[0]; const organizer = form.querySelector('[data-market-definition-organizer]')?.selectedOptions[0]; const website = result.market.website_url || 'Nicht hinterlegt'; trigger.textContent = result.market.name; trigger.dataset.marketDefinitionName = result.market.name; trigger.dataset.marketDefinitionCategoryId = String(result.market.category_id); trigger.dataset.marketDefinitionCategoryName = category?.textContent || ''; trigger.dataset.marketDefinitionOrganizerId = String(result.market.organizer_id); trigger.dataset.marketDefinitionOrganizerName = organizer?.textContent || ''; trigger.dataset.marketDefinitionWebsite = website; showReadCard(trigger); } else { appendMarketRow(result.market); closeDrawer(); } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'; } } finally { if (submit) { submit.disabled = false; } } });";
|
||||
echo "}";
|
||||
echo "function initBestellungenBindings() {";
|
||||
echo " const contentRoot = document.querySelector('[data-left-navigation-content-body]');";
|
||||
echo " if (!contentRoot) { return; }";
|
||||
@@ -1149,12 +1403,12 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
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 value = (label, content) => '<p class=\"sg-body\"><strong>' + escapeHtml(label) + ':</strong> ' + escapeHtml(content || 'Nicht vorhanden') + '</p>';";
|
||||
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).join('<br>');";
|
||||
echo " const lines = (order.lines || []).map((line) => '<div class=\"sg-card-segment sg-card-segment--body\"><p class=\"sg-body\"><strong>' + escapeHtml(line.item_name) + '</strong> · ' + escapeHtml(line.item_code) + ' · Menge: ' + escapeHtml(String(line.qty)) + '</p>' + value('Positionsstatus', line.line_status) + value('Einzelpreis', formatAmount(line.unit_price, a.currency || 'CHF')) + value('Positionssumme', formatAmount(line.line_total, a.currency || 'CHF')) + '</div>').join('');";
|
||||
echo " const allocations = (order.lines || []).flatMap((line) => (line.product_allocations || []).map((item) => '<div class=\"sg-body\"><strong>' + escapeHtml(item.product_name) + ':</strong> ' + escapeHtml(item.lot_number || 'Nicht zugeordnet') + ' · ' + escapeHtml(item.allocated_qty === null ? 'Nicht zugeordnet' : String(item.allocated_qty)) + ' · ' + escapeHtml(item.allocation_status || 'Nicht vorhanden') + (item.lot_status === 'virtual_shortage' ? ' · Virtuelle Fehlbestandscharge' : '') + '</div>')).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\">' + 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) + '<p class=\"sg-body\"><strong>Kunde:</strong> ' + escapeHtml([c.first_name, c.last_name].filter(Boolean).join(' ') || 'Nicht vorhanden') + '</p><p class=\"sg-body\"><strong>Lieferadresse:</strong><br>' + address + '</p>' + 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')) + '</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 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 " };";
|
||||
echo " setDrawerState(true);";
|
||||
echo " if (drawerTitle) { drawerTitle.textContent = trigger.dataset.orderNumber ? 'Bestellung ' + trigger.dataset.orderNumber : 'Bestellung'; }";
|
||||
@@ -1409,6 +1663,10 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const houseNumberInput = form.querySelector('[data-otc-order-house-number]');";
|
||||
echo " const zipInput = form.querySelector('[data-otc-order-zip]');";
|
||||
echo " const cityInput = form.querySelector('[data-otc-order-city]');";
|
||||
echo " const marketLinkToggle = form.querySelector('[data-otc-market-link-toggle]');";
|
||||
echo " const marketLinkEnabledInput = form.querySelector('[data-otc-market-link-enabled]');";
|
||||
echo " const marketOccurrenceInput = form.querySelector('[data-otc-market-occurrence]');";
|
||||
echo " const marketDayInput = form.querySelector('[data-otc-market-day]');";
|
||||
echo " const submitBtn = overlay.querySelector('[data-otc-order-submit]');";
|
||||
echo " const errorEl = form.querySelector('[data-otc-order-error]');";
|
||||
echo " const successEl = form.querySelector('[data-otc-order-success]');";
|
||||
@@ -1419,6 +1677,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const confirmationQuantity = overlay.querySelector('[data-otc-confirmation-quantity]');";
|
||||
echo " const confirmationTotal = overlay.querySelector('[data-otc-confirmation-total]');";
|
||||
echo " const confirmationSubmit = overlay.querySelector('[data-otc-confirmation-submit]');";
|
||||
echo " const confirmationError = overlay.querySelector('[data-otc-confirmation-error]');";
|
||||
echo " let pendingOrderData = null;";
|
||||
echo " const openTriggers = () => document.querySelectorAll('[data-otc-order-open]');";
|
||||
echo " const setTriggerState = (expanded) => {";
|
||||
@@ -1426,6 +1685,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " };";
|
||||
echo " const clearError = () => {";
|
||||
echo " if (errorEl) { errorEl.textContent = ''; errorEl.classList.add('hidden'); }";
|
||||
echo " if (confirmationError) { confirmationError.textContent = ''; confirmationError.classList.add('hidden'); }";
|
||||
echo " };";
|
||||
echo " const clearSuccess = () => {";
|
||||
echo " if (successEl) { successEl.textContent = successDefaultText; successEl.classList.add('hidden'); }";
|
||||
@@ -1462,6 +1722,10 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " };";
|
||||
echo " const getInputValue = (input) => parseInt(input.value || '0', 10) || 0;";
|
||||
echo " const getFieldValue = (input) => (input ? input.value.trim() : '');";
|
||||
echo " const isMarketLinkEnabled = () => marketLinkToggle?.getAttribute('aria-checked') === 'true';";
|
||||
echo " const setDisabledSelectState = (select, disabled) => { if (!select) { return; } select.disabled = disabled; select.setAttribute('aria-disabled', String(disabled)); select.classList.toggle('sg-form-disabled', disabled); select.classList.toggle('sg-input-single-line--disabled', disabled); select.classList.toggle('sg-pulldown--disabled', disabled); select.classList.toggle('sg-form-inactive-selectable', !disabled); select.classList.toggle('sg-input-single-line--inactive-selectable', !disabled); };";
|
||||
echo " const syncMarketDayOptions = () => { if (!marketOccurrenceInput || !marketDayInput) { return; } const marketDefinitionId = marketOccurrenceInput.value; Array.from(marketDayInput.options).forEach((option, index) => { if (index === 0) { option.hidden = false; option.disabled = false; return; } const matches = marketDefinitionId !== '' && option.dataset.marketDefinitionId === marketDefinitionId; option.hidden = !matches; option.disabled = !matches; }); if (!marketDefinitionId || !Array.from(marketDayInput.options).some((option) => option.value === marketDayInput.value && !option.disabled)) { marketDayInput.value = ''; } setDisabledSelectState(marketDayInput, !isMarketLinkEnabled() || marketDefinitionId === ''); };";
|
||||
echo " const syncMarketLinkState = () => { const enabled = isMarketLinkEnabled(); if (marketLinkEnabledInput) { marketLinkEnabledInput.value = enabled ? '1' : '0'; } setDisabledSelectState(marketOccurrenceInput, !enabled); if (!enabled && marketOccurrenceInput) { marketOccurrenceInput.value = ''; } syncMarketDayOptions(); };";
|
||||
echo " const updatePaymentAddButton = () => { addPaymentButton.disabled = paymentRows.querySelectorAll('[data-otc-payment-row]').length >= paymentOptionValues.length; };";
|
||||
echo " const updateFormState = () => {";
|
||||
echo " const totalQty = productInputs.reduce((sum, input) => sum + getInputValue(input), 0);";
|
||||
@@ -1473,7 +1737,8 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " && !!getFieldValue(streetInput)";
|
||||
echo " && !!getFieldValue(houseNumberInput)";
|
||||
echo " && !!getFieldValue(zipInput)";
|
||||
echo " && !!getFieldValue(cityInput);";
|
||||
echo " && !!getFieldValue(cityInput)";
|
||||
echo " && (!isMarketLinkEnabled() || (!!getFieldValue(marketOccurrenceInput) && !!getFieldValue(marketDayInput)));";
|
||||
echo " let errorMsg = '';";
|
||||
echo " if (totalQty === 0) {";
|
||||
echo " errorMsg = 'Mindestens ein Produkt mit Menge > 0 erforderlich.';";
|
||||
@@ -1481,6 +1746,8 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " errorMsg = 'Bestelldatum auswählen.';";
|
||||
echo " } else if (!getFieldValue(firstNameInput) || !getFieldValue(lastNameInput) || !getFieldValue(streetInput) || !getFieldValue(houseNumberInput) || !getFieldValue(zipInput) || !getFieldValue(cityInput)) {";
|
||||
echo " errorMsg = 'Alle Rechnungsadress-Felder ausfüllen.';";
|
||||
echo " } else if (isMarketLinkEnabled() && (!getFieldValue(marketOccurrenceInput) || !getFieldValue(marketDayInput))) {";
|
||||
echo " errorMsg = 'Markt und Markttag auswählen.';";
|
||||
echo " }";
|
||||
echo " if (errorEl) {";
|
||||
echo " errorEl.textContent = errorMsg;";
|
||||
@@ -1497,6 +1764,8 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " updatePaymentAddButton();";
|
||||
echo " clearError();";
|
||||
echo " if (!preserveSuccess) { clearSuccess(); }";
|
||||
echo " if (marketLinkToggle) { marketLinkToggle.setAttribute('aria-checked', 'false'); marketLinkToggle.classList.remove('sg-form-active'); marketLinkToggle.classList.add('sg-checkbox-field--inactive-selectable'); }";
|
||||
echo " syncMarketLinkState();";
|
||||
echo " if (!preserveSuccess) { updateFormState(); }";
|
||||
echo " };";
|
||||
echo " const closeConfirmation = () => {";
|
||||
@@ -1563,6 +1832,9 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " addPaymentButton.addEventListener('click', addPaymentRow);";
|
||||
echo " bindPaymentRow(paymentRows.querySelector('[data-otc-payment-row]'));";
|
||||
echo " updatePaymentAddButton();";
|
||||
echo " marketLinkToggle?.addEventListener('click', (event) => { event.stopImmediatePropagation(); const enabled = marketLinkToggle.getAttribute('aria-checked') !== 'true'; marketLinkToggle.setAttribute('aria-checked', String(enabled)); marketLinkToggle.classList.toggle('sg-form-active', enabled); marketLinkToggle.classList.toggle('sg-checkbox-field--inactive-selectable', !enabled); syncMarketLinkState(); updateFormState(); });";
|
||||
echo " marketOccurrenceInput?.addEventListener('change', () => { syncMarketDayOptions(); updateFormState(); });";
|
||||
echo " marketDayInput?.addEventListener('change', updateFormState);";
|
||||
echo " const submitOrder = async (orderData) => {";
|
||||
echo " let submissionSucceeded = false;";
|
||||
echo " const originalBtnText = confirmationSubmit.innerHTML;";
|
||||
@@ -1585,17 +1857,15 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " closeConfirmation();";
|
||||
echo " resetForm();";
|
||||
echo " toggleOverlay(false);";
|
||||
echo " } else if (errorEl) {";
|
||||
echo " errorEl.textContent = result.error || 'Unbekannter Fehler';";
|
||||
echo " errorEl.classList.remove('hidden');";
|
||||
echo " closeConfirmation();";
|
||||
echo " } else {";
|
||||
echo " const message = result.error || 'Unbekannter Fehler';";
|
||||
echo " if (errorEl) { errorEl.textContent = message; errorEl.classList.remove('hidden'); }";
|
||||
echo " if (confirmationError) { confirmationError.textContent = message; confirmationError.classList.remove('hidden'); }";
|
||||
echo " }";
|
||||
echo " } catch (error) {";
|
||||
echo " if (errorEl) {";
|
||||
echo " errorEl.textContent = 'Netzwerkfehler: ' + error.message;";
|
||||
echo " errorEl.classList.remove('hidden');";
|
||||
echo " }";
|
||||
echo " closeConfirmation();";
|
||||
echo " const message = 'Netzwerkfehler: ' + error.message;";
|
||||
echo " if (errorEl) { errorEl.textContent = message; errorEl.classList.remove('hidden'); }";
|
||||
echo " if (confirmationError) { confirmationError.textContent = message; confirmationError.classList.remove('hidden'); }";
|
||||
echo " } finally {";
|
||||
echo " confirmationSubmit.innerHTML = originalBtnText;";
|
||||
echo " confirmationSubmit.disabled = false;";
|
||||
@@ -1612,14 +1882,16 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " products: products,";
|
||||
echo " orderDate: orderDateInput ? orderDateInput.value : '',";
|
||||
echo " payments: Array.from(paymentRows.querySelectorAll('[data-otc-payment-row]')).map((row) => ({ paymentMethod: row.querySelector('[data-otc-payment-method]').value, amount: parseFloat(row.querySelector('[data-otc-payment-amount]').value || '0') || 0 })),";
|
||||
echo " billing: {";
|
||||
echo " billing: {";
|
||||
echo " firstName: getFieldValue(firstNameInput),";
|
||||
echo " lastName: getFieldValue(lastNameInput),";
|
||||
echo " street: getFieldValue(streetInput),";
|
||||
echo " houseNumber: getFieldValue(houseNumberInput),";
|
||||
echo " zip: getFieldValue(zipInput),";
|
||||
echo " city: getFieldValue(cityInput)";
|
||||
echo " }";
|
||||
echo " city: getFieldValue(cityInput)";
|
||||
echo " },";
|
||||
echo " marketOccurrenceId: isMarketLinkEnabled() ? (marketDayInput?.selectedOptions[0]?.dataset.marketOccurrenceId || null) : null,";
|
||||
echo " marketOccurrenceDayId: isMarketLinkEnabled() ? getFieldValue(marketDayInput) : null";
|
||||
echo " };";
|
||||
echo " openConfirmation(orderData);";
|
||||
echo " });";
|
||||
@@ -1627,6 +1899,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " if (event.target.closest('[data-otc-confirmation-cancel]')) { closeConfirmation(); return; }";
|
||||
echo " if (event.target.closest('[data-otc-confirmation-submit]') && pendingOrderData) { submitOrder(pendingOrderData); }";
|
||||
echo " });";
|
||||
echo " syncMarketLinkState();";
|
||||
echo " updateFormState();";
|
||||
echo "})();";
|
||||
echo "if (window.sgInitHelpIconOverlays) {";
|
||||
@@ -1635,8 +1908,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " outsideClickIgnoreSelectors: ['.sg-sandwich-menu-wrap'],";
|
||||
echo " });";
|
||||
echo "}";
|
||||
echo "renderMainHeading('ERP');";
|
||||
echo "renderLeftNavigation('ERP');";
|
||||
echo "(() => { const url = new URL(window.location.href); const requestedModule = url.searchParams.get('portal_module') || 'ERP'; const moduleButton = Array.from(document.querySelectorAll('.sg-portal-header__tabs .sg-tab-button')).find((button) => button.textContent.trim() === requestedModule) || document.querySelector('.sg-portal-header__tabs .sg-tab-button'); if (!moduleButton) { return; } const moduleName = moduleButton.textContent.trim(); document.querySelectorAll('.sg-portal-header__tabs .sg-tab-button').forEach((button) => { const active = button === moduleButton; button.setAttribute('aria-selected', String(active)); button.dataset.componentState = active ? 'active' : 'inactive'; }); renderMainHeading(moduleName); renderLeftNavigation(moduleName, url.searchParams.get('portal_section') || ''); })();";
|
||||
echo '</script>';
|
||||
echo '</body>';
|
||||
echo '</html>';
|
||||
|
||||
@@ -10,7 +10,7 @@ function throttle_webhook_channel(string $channel, int $minIntervalSeconds): voi
|
||||
$sanitizedChannel = preg_replace('/[^a-z0-9_-]+/i', '_', $channel) ?? 'default';
|
||||
$lockPath = sys_get_temp_dir() . '/erp_naurua_webhook_throttle_' . $sanitizedChannel . '.lock';
|
||||
|
||||
$handle = fopen($lockPath, 'c+');
|
||||
$handle = @fopen($lockPath, 'c+');
|
||||
if ($handle === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ require_once __DIR__ . '/../shared/db.php';
|
||||
require_once __DIR__ . '/../erp/lager/forecast.php';
|
||||
require_once __DIR__ . '/../erp/import-integration/shopify-webhook-worker.php';
|
||||
require_once __DIR__ . '/../erp/import-integration/shopify-delta-run.php';
|
||||
require_once __DIR__ . '/../market_management/service.php';
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
@@ -42,6 +43,7 @@ try {
|
||||
}
|
||||
},
|
||||
'erp.lager.stock_lot_forecast_refresh' => static fn (): array => run_lager_stock_lot_forecast($pdo),
|
||||
'market_management.occurrence_status_completion' => static fn (): array => market_management_auto_complete_occurrences($pdo),
|
||||
];
|
||||
$results = [];
|
||||
foreach ($jobs as $name => $job) {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../modules/shared/db.php';
|
||||
require_once __DIR__ . '/../../modules/shared/auth/service.php';
|
||||
require_once __DIR__ . '/../../modules/market_management/service.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
$env = expand_env_values(parse_env_file(__DIR__ . '/../../.env'));
|
||||
$pdo = connect_database($env);
|
||||
auth_bootstrap_session();
|
||||
|
||||
if (auth_current_user($pdo) === null) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['ok' => false, 'error' => 'Nicht autorisiert.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['ok' => false, 'error' => 'Methode nicht erlaubt.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$csrf = (string) ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
|
||||
if (!auth_validate_csrf_token($csrf)) {
|
||||
http_response_code(419);
|
||||
echo json_encode(['ok' => false, 'error' => 'Ungültiges Sicherheits-Token.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$payload = json_decode((string) file_get_contents('php://input'), true);
|
||||
if (!is_array($payload)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'Ungültige Anfrage.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$fields = is_array($payload['fields'] ?? null) ? $payload['fields'] : [];
|
||||
if (($fields['organizer_id'] ?? '') === '__new__') {
|
||||
$organizer = market_management_create_organizer(
|
||||
$pdo,
|
||||
(string) ($fields['organizer_name'] ?? ''),
|
||||
(string) ($fields['organizer_email'] ?? ''),
|
||||
(string) ($fields['organizer_phone'] ?? '')
|
||||
);
|
||||
$fields['organizer_id'] = $organizer['id'];
|
||||
}
|
||||
if (($payload['action'] ?? 'create') === 'delete') {
|
||||
$marketId = (int) ($payload['id'] ?? 0);
|
||||
if ($marketId < 1) {
|
||||
throw new InvalidArgumentException('id ist erforderlich.');
|
||||
}
|
||||
if (($payload['confirmation'] ?? '') !== 'DELETE') {
|
||||
throw new InvalidArgumentException('Die Löschbestätigung muss exakt DELETE lauten.');
|
||||
}
|
||||
$market = market_management_set_market_archive($pdo, $marketId, true);
|
||||
} elseif (($payload['action'] ?? 'create') === 'update') {
|
||||
$marketId = (int) ($payload['id'] ?? 0);
|
||||
if ($marketId < 1) {
|
||||
throw new InvalidArgumentException('id ist erforderlich.');
|
||||
}
|
||||
$market = market_management_update_market($pdo, $marketId, [
|
||||
'name' => $fields['name'] ?? null,
|
||||
'category_id' => $fields['category_id'] ?? null,
|
||||
'organizer_id' => $fields['organizer_id'] ?? null,
|
||||
'website_url' => $fields['website_url'] ?? null,
|
||||
]);
|
||||
} else {
|
||||
$market = market_management_create_market($pdo, [
|
||||
'name' => $fields['name'] ?? null,
|
||||
'category_id' => $fields['category_id'] ?? null,
|
||||
'organizer_id' => $fields['organizer_id'] ?? null,
|
||||
'website_url' => $fields['website_url'] ?? null,
|
||||
]);
|
||||
}
|
||||
echo json_encode(['ok' => true, 'market' => $market], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} catch (Throwable $error) {
|
||||
http_response_code(422);
|
||||
echo json_encode(['ok' => false, 'error' => $error->getMessage()], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
@@ -4,6 +4,19 @@ declare(strict_types=1);
|
||||
require_once __DIR__ . '/../../modules/shared/db.php';
|
||||
require_once __DIR__ . '/../../modules/market_management/mcp.php';
|
||||
|
||||
final class MarketManagementMcpJsonRpcException extends RuntimeException
|
||||
{
|
||||
public function __construct(private int $rpcCode, string $message, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
|
||||
public function rpcCode(): int
|
||||
{
|
||||
return $this->rpcCode;
|
||||
}
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
@@ -18,6 +31,10 @@ try {
|
||||
throw new InvalidArgumentException('Ungültige JSON-RPC-Anfrage.');
|
||||
}
|
||||
|
||||
if (array_key_exists('params', $request) && !is_array($request['params'])) {
|
||||
throw new MarketManagementMcpJsonRpcException(-32602, 'Parameter müssen als Objekt oder Array übergeben werden.');
|
||||
}
|
||||
|
||||
$id = $request['id'] ?? null;
|
||||
$method = (string) ($request['method'] ?? '');
|
||||
$params = is_array($request['params'] ?? null) ? $request['params'] : [];
|
||||
@@ -36,23 +53,35 @@ try {
|
||||
} elseif ($method === 'tools/call') {
|
||||
$tool = (string) ($params['name'] ?? '');
|
||||
$arguments = is_array($params['arguments'] ?? null) ? $params['arguments'] : [];
|
||||
$result = [
|
||||
'content' => [[
|
||||
'type' => 'text',
|
||||
'text' => json_encode(market_management_mcp_call_idempotent($pdo, $userSubject, $tool, $arguments), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
|
||||
]],
|
||||
'isError' => false,
|
||||
];
|
||||
if (array_key_exists('arguments', $params) && !is_array($params['arguments'])) {
|
||||
throw new MarketManagementMcpJsonRpcException(-32602, 'Tool-Argumente müssen als Objekt übergeben werden.');
|
||||
}
|
||||
try {
|
||||
$result = [
|
||||
'content' => [[
|
||||
'type' => 'text',
|
||||
'text' => json_encode(market_management_mcp_call_idempotent($pdo, $userSubject, $tool, $arguments), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
|
||||
]],
|
||||
'isError' => false,
|
||||
];
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new MarketManagementMcpJsonRpcException(-32602, $e->getMessage(), $e);
|
||||
}
|
||||
} else {
|
||||
throw new InvalidArgumentException('Unbekannte JSON-RPC-Methode: ' . $method);
|
||||
throw new MarketManagementMcpJsonRpcException(-32601, 'Unbekannte JSON-RPC-Methode: ' . $method);
|
||||
}
|
||||
|
||||
echo json_encode(['jsonrpc' => '2.0', 'id' => $id, 'result' => $result], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code($e instanceof RuntimeException && str_contains($e->getMessage(), 'Token') ? 401 : 400);
|
||||
$isAuthError = $e instanceof RuntimeException && str_contains($e->getMessage(), 'Token');
|
||||
$isJsonRpcError = $e instanceof MarketManagementMcpJsonRpcException;
|
||||
http_response_code($isJsonRpcError ? 200 : ($isAuthError ? 401 : 400));
|
||||
echo json_encode([
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => $request['id'] ?? null,
|
||||
'error' => ['code' => -32600, 'message' => $e->getMessage()],
|
||||
'error' => [
|
||||
'code' => $isJsonRpcError ? $e->rpcCode() : -32600,
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../modules/shared/db.php';
|
||||
require_once __DIR__ . '/../../modules/shared/auth/service.php';
|
||||
require_once __DIR__ . '/../../modules/erp/kontakte/service.php';
|
||||
require_once __DIR__ . '/../../modules/market_management/service.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
@@ -32,17 +33,86 @@ if ($requestMethod === 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'Ungültige Anfrage']);
|
||||
exit;
|
||||
}
|
||||
$scope = (string) ($payload['scope'] ?? '');
|
||||
$id = market_management_require_positive_id($payload['id'] ?? null, 'id');
|
||||
$fields = is_array($payload['fields'] ?? null) ? $payload['fields'] : [];
|
||||
if ($scope === 'occurrence') {
|
||||
market_management_update_occurrence($pdo, $id, $fields);
|
||||
} elseif ($scope === 'day') {
|
||||
market_management_update_day($pdo, $id, $fields);
|
||||
} else {
|
||||
throw new InvalidArgumentException('Ungültiger Bearbeitungsbereich.');
|
||||
try {
|
||||
if (($payload['action'] ?? '') === 'create') {
|
||||
$fields = is_array($payload['fields'] ?? null) ? $payload['fields'] : [];
|
||||
$occurrence = market_management_create_occurrence($pdo, [
|
||||
'market_definition_id' => $fields['market_definition_id'] ?? null,
|
||||
'days' => [['date' => $fields['date'] ?? null]],
|
||||
]);
|
||||
echo json_encode(['ok' => true, 'occurrence_id' => (int) $occurrence['id']], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
$scope = (string) ($payload['scope'] ?? '');
|
||||
$id = market_management_require_positive_id($payload['id'] ?? null, 'id');
|
||||
$fields = is_array($payload['fields'] ?? null) ? $payload['fields'] : [];
|
||||
if (($payload['action'] ?? '') === 'delete') {
|
||||
if (($payload['confirmation'] ?? '') !== 'DELETE') {
|
||||
throw new InvalidArgumentException('Die Löschbestätigung muss exakt DELETE lauten.');
|
||||
}
|
||||
market_management_set_occurrence_archive($pdo, $id, true);
|
||||
} elseif (($payload['action'] ?? '') === 'create_day') {
|
||||
$fields['occurrence_id'] = $id;
|
||||
$day = market_management_create_day($pdo, $fields);
|
||||
echo json_encode(['ok' => true, 'day' => $day]);
|
||||
exit;
|
||||
} elseif (($payload['action'] ?? '') === 'delete_day') {
|
||||
if (($payload['confirmation'] ?? '') !== 'DELETE') {
|
||||
throw new InvalidArgumentException('Bestätigung muss exakt DELETE lauten.');
|
||||
}
|
||||
$day = market_management_delete_day($pdo, $id);
|
||||
echo json_encode(['ok' => true, 'day' => $day]);
|
||||
exit;
|
||||
} elseif ($scope === 'day') {
|
||||
market_management_update_day($pdo, $id, $fields);
|
||||
} elseif (in_array($scope, ['occurrence', 'status', 'overview', 'participation', 'infrastructure', 'travel', 'notes'], true)) {
|
||||
$contactFields = [];
|
||||
foreach (['contact_person_name', 'contact_email', 'contact_phone'] as $contactField) {
|
||||
if (array_key_exists($contactField, $fields)) {
|
||||
$contactFields[$contactField] = $fields[$contactField];
|
||||
unset($fields[$contactField]);
|
||||
}
|
||||
}
|
||||
$existingOccurrence = null;
|
||||
if ($contactFields !== []) {
|
||||
$existingOccurrence = market_management_get_occurrence($pdo, $id, true);
|
||||
}
|
||||
if ($contactFields !== []) {
|
||||
$contactFields['occurrence_id'] = $id;
|
||||
market_management_set_occurrence_contact($pdo, $contactFields);
|
||||
}
|
||||
if ($fields !== []) {
|
||||
$simpleCostFields = [];
|
||||
foreach (['gross_cost', 'cost_items_text', 'tax_status', 'tax_rate'] as $costField) {
|
||||
if (array_key_exists($costField, $fields)) {
|
||||
$simpleCostFields[$costField] = $fields[$costField];
|
||||
unset($fields[$costField]);
|
||||
}
|
||||
}
|
||||
if ($simpleCostFields !== []) {
|
||||
market_management_update_occurrence_simple_costs($pdo, $id, $simpleCostFields);
|
||||
}
|
||||
if ($fields !== []) {
|
||||
market_management_update_occurrence($pdo, $id, $fields);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new InvalidArgumentException('Ungültiger Bearbeitungsbereich.');
|
||||
}
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
} catch (Throwable $error) {
|
||||
http_response_code(422);
|
||||
echo json_encode(['ok' => false, 'error' => $error->getMessage()], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
echo json_encode(['ok' => true]);
|
||||
}
|
||||
|
||||
if (($requestMethod === 'GET') && ($_GET['action'] ?? '') === 'create_options') {
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'market_definitions' => market_management_list_active_market_definitions($pdo),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
--color-process-inactive: #FFAE79;
|
||||
--color-signal-green: #009101;
|
||||
--color-signal-green-light: #65A665;
|
||||
--color-signal-green-red: #C0624D;
|
||||
--color-signal-yellow: #9C7A00;
|
||||
--color-ampel-hellgelb: #D3B962;
|
||||
--color-signal-red: #9B3B2F;
|
||||
@@ -37,6 +38,7 @@
|
||||
--surface-card-header-muted: var(--color-darkbrown);
|
||||
--divider-card-segment: var(--color-white);
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-signal: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-dark);
|
||||
--text-card-transparent: var(--color-font-light);
|
||||
--layout-card-body-content-justify: flex-start;
|
||||
@@ -62,6 +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-pulldown-panel: var(--color-light-grey);
|
||||
--surface-activatable-remove: var(--surface-control-default);
|
||||
--surface-slider-track: var(--color-medium-grey);
|
||||
@@ -89,6 +92,8 @@
|
||||
--layout-mode-toggle-local-width-factor: 3;
|
||||
--layout-mode-toggle-width: calc(var(--interaction-height) * 3.5);
|
||||
--layout-help-panel-width: calc(var(--interaction-height) * 8);
|
||||
--layout-tooltip-width: calc(var(--interaction-height) * 8);
|
||||
--radius-tooltip: var(--radius-interaction);
|
||||
--text-control-default: var(--color-font-dark);
|
||||
--text-control-disabled: var(--color-dark-grey);
|
||||
--text-button-process: var(--color-font-light);
|
||||
@@ -98,6 +103,7 @@
|
||||
--text-menu-link-portal: var(--color-font-dark);
|
||||
--text-help-icon: var(--color-font-dark);
|
||||
--text-help-panel: var(--color-font-dark);
|
||||
--text-tooltip: var(--color-font-dark);
|
||||
--text-activatable-remove: var(--text-control-default);
|
||||
--text-hyperlink: var(--color-font-hyperlink);
|
||||
--icon-pulldown-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M2 2l4 4 4-4' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='square' stroke-linejoin='miter'/%3E%3C/svg%3E");
|
||||
@@ -140,7 +146,6 @@
|
||||
--layout-preview-align-items: flex-start;
|
||||
|
||||
/* Semantic pattern tokens: Portal header */
|
||||
--surface-portal-background: var(--color-background-purple);
|
||||
--surface-portal-header: var(--color-darkblue);
|
||||
--surface-portal-header-tab: var(--color-midblue);
|
||||
--surface-portal-header-tab-active: var(--color-light-grey);
|
||||
@@ -306,7 +311,6 @@
|
||||
--dimension-input-label-width: 9rem;
|
||||
--dimension-input-field-desktop-width: 400px;
|
||||
--dimension-input-field-max-width: 600px;
|
||||
--dimension-email-body-max-width: 600px;
|
||||
--dimension-search-field-width: 15.3rem;
|
||||
--dimension-public-content-width: 100%;
|
||||
--dimension-layer-pulldown-panel: 40;
|
||||
@@ -354,7 +358,7 @@ body {
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-base);
|
||||
color: var(--color-font-dark);
|
||||
background: var(--surface-portal-background);
|
||||
background: var(--color-background-purple);
|
||||
margin: 0;
|
||||
padding: var(--spacing-small);
|
||||
}
|
||||
@@ -1760,6 +1764,134 @@ section + section {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel {
|
||||
position: absolute;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
display: block;
|
||||
width: var(--layout-tooltip-width);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: anywhere;
|
||||
padding: var(--spacing-large);
|
||||
border-radius: var(--radius-tooltip);
|
||||
color: var(--text-tooltip);
|
||||
background: var(--surface-tooltip);
|
||||
box-shadow: var(--shadow-overlay);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel {
|
||||
top: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel {
|
||||
bottom: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
right: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
left: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel::before {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel::before {
|
||||
top: calc(var(--spacing-small) * -1);
|
||||
left: 50%;
|
||||
border-width: 0 var(--spacing-small) var(--spacing-small);
|
||||
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);
|
||||
left: 50%;
|
||||
border-width: var(--spacing-small) var(--spacing-small) 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);
|
||||
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;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"] {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="bottom"]::before {
|
||||
top: calc(var(--spacing-small) * -1);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent var(--surface-tooltip);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="top"]::before {
|
||||
bottom: calc(var(--spacing-small) * -1);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: var(--surface-tooltip) transparent transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="left"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
right: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent transparent var(--surface-tooltip);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="right"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
left: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-pulldown-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + var(--spacing-small));
|
||||
@@ -2248,6 +2380,52 @@ section + section {
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Pattern: History Graph */
|
||||
/* ========================================================= */
|
||||
|
||||
.sg-history-graph {
|
||||
--layout-history-graph-connector-width: var(--score-marker-width);
|
||||
--surface-history-graph-connector: var(--chart-marker-line);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sg-history-graph__event {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__event:not(:last-child)::after {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
width: var(--layout-history-graph-connector-width);
|
||||
height: var(--spacing-large);
|
||||
background: var(--surface-history-graph-connector);
|
||||
content: "";
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-history-graph__card {
|
||||
width: 100%;
|
||||
max-width: var(--layout-object-card-min-width);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.sg-history-graph__navigation {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Patterns: Portal Header */
|
||||
/* ========================================================= */
|
||||
@@ -2449,19 +2627,39 @@ section + section {
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left {
|
||||
order: 0;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-pulldown-demo,
|
||||
.sg-options-row__left > .sg-input-single-line-wrap,
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 1 1 calc(50% - var(--layout-options-row-group-gap));
|
||||
.sg-options-row__left > .sg-pulldown-demo {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left:has(> .sg-pulldown-demo ~ .sg-pulldown-demo) > .sg-pulldown-demo {
|
||||
flex-basis: calc(50% - (var(--layout-options-row-group-gap) / 2));
|
||||
}
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left > .sg-pulldown-demo {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 0 0 auto;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row > .sg-search-field-input {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row .sg-input-single-line {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-input-single-line-wrap .sg-input-single-line,
|
||||
@@ -4215,22 +4413,27 @@ main {
|
||||
|
||||
.sg-card-segment--signal-red {
|
||||
background: var(--color-signal-red);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green {
|
||||
background: var(--color-signal-green);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-light {
|
||||
background: var(--color-signal-green-light);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-red {
|
||||
background: var(--color-signal-green-red);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-yellow {
|
||||
background: var(--color-signal-yellow);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--gray {
|
||||
@@ -4250,6 +4453,11 @@ main {
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card.sg-card--notification-signal-green-red {
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card--content-card {
|
||||
--surface-card-header-primary: var(--surface-content-block-card-title);
|
||||
--surface-card-body: var(--surface-content-block-card-content);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"styleguideVersion": "2026.05.18.1",
|
||||
"styleguideCommit": "71734d6",
|
||||
"syncedAtUtc": "2026-09-08T09:49:27Z",
|
||||
"styleguideVersion": "2026.05.18.2",
|
||||
"styleguideCommit": "519ed6b",
|
||||
"syncedAtUtc": "2026-09-24T11:09:21Z",
|
||||
"sourceRepo": "/Users/mathias/Documents/Dokumente Chouchou/Codebases/Styleguide",
|
||||
"mirroredDocsPath": "docs/styleguide"
|
||||
}
|
||||
+220
-12
@@ -16,6 +16,7 @@
|
||||
--color-process-inactive: #FFAE79;
|
||||
--color-signal-green: #009101;
|
||||
--color-signal-green-light: #65A665;
|
||||
--color-signal-green-red: #C0624D;
|
||||
--color-signal-yellow: #9C7A00;
|
||||
--color-ampel-hellgelb: #D3B962;
|
||||
--color-signal-red: #9B3B2F;
|
||||
@@ -37,6 +38,7 @@
|
||||
--surface-card-header-muted: var(--color-darkbrown);
|
||||
--divider-card-segment: var(--color-white);
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-signal: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-dark);
|
||||
--text-card-transparent: var(--color-font-light);
|
||||
--layout-card-body-content-justify: flex-start;
|
||||
@@ -62,6 +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-pulldown-panel: var(--color-light-grey);
|
||||
--surface-activatable-remove: var(--surface-control-default);
|
||||
--surface-slider-track: var(--color-medium-grey);
|
||||
@@ -89,6 +92,8 @@
|
||||
--layout-mode-toggle-local-width-factor: 3;
|
||||
--layout-mode-toggle-width: calc(var(--interaction-height) * 3.5);
|
||||
--layout-help-panel-width: calc(var(--interaction-height) * 8);
|
||||
--layout-tooltip-width: calc(var(--interaction-height) * 8);
|
||||
--radius-tooltip: var(--radius-interaction);
|
||||
--text-control-default: var(--color-font-dark);
|
||||
--text-control-disabled: var(--color-dark-grey);
|
||||
--text-button-process: var(--color-font-light);
|
||||
@@ -98,6 +103,7 @@
|
||||
--text-menu-link-portal: var(--color-font-dark);
|
||||
--text-help-icon: var(--color-font-dark);
|
||||
--text-help-panel: var(--color-font-dark);
|
||||
--text-tooltip: var(--color-font-dark);
|
||||
--text-activatable-remove: var(--text-control-default);
|
||||
--text-hyperlink: var(--color-font-hyperlink);
|
||||
--icon-pulldown-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M2 2l4 4 4-4' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='square' stroke-linejoin='miter'/%3E%3C/svg%3E");
|
||||
@@ -140,7 +146,6 @@
|
||||
--layout-preview-align-items: flex-start;
|
||||
|
||||
/* Semantic pattern tokens: Portal header */
|
||||
--surface-portal-background: var(--color-background-purple);
|
||||
--surface-portal-header: var(--color-darkblue);
|
||||
--surface-portal-header-tab: var(--color-midblue);
|
||||
--surface-portal-header-tab-active: var(--color-light-grey);
|
||||
@@ -306,7 +311,6 @@
|
||||
--dimension-input-label-width: 9rem;
|
||||
--dimension-input-field-desktop-width: 400px;
|
||||
--dimension-input-field-max-width: 600px;
|
||||
--dimension-email-body-max-width: 600px;
|
||||
--dimension-search-field-width: 15.3rem;
|
||||
--dimension-public-content-width: 100%;
|
||||
--dimension-layer-pulldown-panel: 40;
|
||||
@@ -354,7 +358,7 @@ body {
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-base);
|
||||
color: var(--color-font-dark);
|
||||
background: var(--surface-portal-background);
|
||||
background: var(--color-background-purple);
|
||||
margin: 0;
|
||||
padding: var(--spacing-small);
|
||||
}
|
||||
@@ -1760,6 +1764,134 @@ section + section {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel {
|
||||
position: absolute;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
display: block;
|
||||
width: var(--layout-tooltip-width);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: anywhere;
|
||||
padding: var(--spacing-large);
|
||||
border-radius: var(--radius-tooltip);
|
||||
color: var(--text-tooltip);
|
||||
background: var(--surface-tooltip);
|
||||
box-shadow: var(--shadow-overlay);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel {
|
||||
top: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel {
|
||||
bottom: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
right: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
left: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel::before {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel::before {
|
||||
top: calc(var(--spacing-small) * -1);
|
||||
left: 50%;
|
||||
border-width: 0 var(--spacing-small) var(--spacing-small);
|
||||
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);
|
||||
left: 50%;
|
||||
border-width: var(--spacing-small) var(--spacing-small) 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);
|
||||
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;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"] {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="bottom"]::before {
|
||||
top: calc(var(--spacing-small) * -1);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent var(--surface-tooltip);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="top"]::before {
|
||||
bottom: calc(var(--spacing-small) * -1);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: var(--surface-tooltip) transparent transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="left"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
right: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent transparent var(--surface-tooltip);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="right"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
left: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-pulldown-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + var(--spacing-small));
|
||||
@@ -2248,6 +2380,52 @@ section + section {
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Pattern: History Graph */
|
||||
/* ========================================================= */
|
||||
|
||||
.sg-history-graph {
|
||||
--layout-history-graph-connector-width: var(--score-marker-width);
|
||||
--surface-history-graph-connector: var(--chart-marker-line);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sg-history-graph__event {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__event:not(:last-child)::after {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
width: var(--layout-history-graph-connector-width);
|
||||
height: var(--spacing-large);
|
||||
background: var(--surface-history-graph-connector);
|
||||
content: "";
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-history-graph__card {
|
||||
width: 100%;
|
||||
max-width: var(--layout-object-card-min-width);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.sg-history-graph__navigation {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Patterns: Portal Header */
|
||||
/* ========================================================= */
|
||||
@@ -2449,19 +2627,39 @@ section + section {
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left {
|
||||
order: 0;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-pulldown-demo,
|
||||
.sg-options-row__left > .sg-input-single-line-wrap,
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 1 1 calc(50% - var(--layout-options-row-group-gap));
|
||||
.sg-options-row__left > .sg-pulldown-demo {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left:has(> .sg-pulldown-demo ~ .sg-pulldown-demo) > .sg-pulldown-demo {
|
||||
flex-basis: calc(50% - (var(--layout-options-row-group-gap) / 2));
|
||||
}
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left > .sg-pulldown-demo {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 0 0 auto;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row > .sg-search-field-input {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row .sg-input-single-line {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-input-single-line-wrap .sg-input-single-line,
|
||||
@@ -4215,22 +4413,27 @@ main {
|
||||
|
||||
.sg-card-segment--signal-red {
|
||||
background: var(--color-signal-red);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green {
|
||||
background: var(--color-signal-green);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-light {
|
||||
background: var(--color-signal-green-light);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-red {
|
||||
background: var(--color-signal-green-red);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-yellow {
|
||||
background: var(--color-signal-yellow);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--gray {
|
||||
@@ -4250,6 +4453,11 @@ main {
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card.sg-card--notification-signal-green-red {
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card--content-card {
|
||||
--surface-card-header-primary: var(--surface-content-block-card-title);
|
||||
--surface-card-body: var(--surface-content-block-card-content);
|
||||
|
||||
@@ -52,6 +52,19 @@ body.sg-card-list-page-drawer-open {
|
||||
background: var(--color-background-purple);
|
||||
}
|
||||
|
||||
.sg-market-definition-mobile-detail-host {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1500;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
padding: var(--spacing-small);
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
background: var(--color-background-purple);
|
||||
}
|
||||
|
||||
.sg-market-mobile-detail-host .sg-card-list-page-drawer {
|
||||
position: static;
|
||||
display: flex;
|
||||
@@ -62,10 +75,24 @@ body.sg-card-list-page-drawer-open {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.sg-market-definition-mobile-detail-host .sg-card-list-page-drawer {
|
||||
position: static;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
body.sg-market-mobile-detail-open > :not([data-market-mobile-detail-host]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.sg-market-definition-mobile-detail-open > :not([data-market-definition-mobile-detail-host]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.market-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(10rem, 0.8fr) minmax(0, 1.2fr);
|
||||
@@ -84,22 +111,24 @@ body.sg-market-mobile-detail-open > :not([data-market-mobile-detail-host]) {
|
||||
}
|
||||
|
||||
.market-detail-row dt {
|
||||
color: var(--color-medium-grey);
|
||||
color: var(--color-black);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.market-detail-row dd {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.market-day-specific {
|
||||
color: var(--color-medium-grey);
|
||||
}
|
||||
|
||||
.market-edit-form .sg-component-row {
|
||||
margin-top: var(--spacing-medium);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.market-edit-form .sg-labeled-input-row .sg-input-multi-line {
|
||||
width: 100%;
|
||||
max-width: var(--layout-form-input-field-max-width);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.market-detail-grid {
|
||||
grid-template-columns: minmax(8rem, 0.9fr) minmax(0, 1.1fr);
|
||||
@@ -313,6 +342,10 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sg-large-table--markets .sg-large-table__row {
|
||||
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));
|
||||
}
|
||||
@@ -354,10 +387,59 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
|
||||
.sg-card-list-page-drawer [data-lager-drawer-topic="new-lot-form"][hidden],
|
||||
.sg-card-list-page-drawer [data-lager-new-lot-notification-wrapper][hidden],
|
||||
.sg-card-list-page-drawer [data-lager-open-edit-form][hidden],
|
||||
.sg-card-list-page-drawer [data-lager-open-card][hidden] {
|
||||
.sg-card-list-page-drawer [data-lager-open-card][hidden],
|
||||
.sg-card-list-page-drawer [data-market-definition-read-card][hidden],
|
||||
.sg-card-list-page-drawer [data-market-definition-create-card][hidden],
|
||||
.sg-card-list-page-drawer [data-market-definition-new-organizer][hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sg-card-list-page-drawer [data-market-new-municipality][hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-shared-editable-card-read][hidden],
|
||||
[data-shared-editable-card-form][hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sg-card-list-page-drawer [data-market-definition-card] .sg-object-card__header > .sg-component-row {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.sg-card-list-page-drawer [data-market-delete-occurrence-id],
|
||||
.sg-card-list-page-drawer [data-market-definition-delete-id] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
--layout-delete-confirmation-overlay-offset-block-start: 100px;
|
||||
}
|
||||
|
||||
.sg-card-list-page-drawer [data-market-delete-occurrence-id] > .sg-component-row,
|
||||
.sg-card-list-page-drawer [data-market-definition-delete-id] > .sg-component-row,
|
||||
.sg-card-list-page-drawer .market-delete-confirmation-card .sg-component-row {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.market-delete-confirmation-card__title {
|
||||
margin: 0;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.market-days-group__title {
|
||||
margin: var(--spacing-small) 0 0;
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.market-required-field-indicator {
|
||||
position: relative;
|
||||
top: -0.25em;
|
||||
margin-left: 0.15em;
|
||||
color: var(--color-signal-red);
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.sg-card-list-page-drawer [data-component="content-group-box"] {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../modules/shared/db.php';
|
||||
require_once __DIR__ . '/../modules/shared/auth/service.php';
|
||||
|
||||
$requestMethod = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
||||
if ($requestMethod !== 'GET') {
|
||||
http_response_code(405);
|
||||
header('Allow: GET');
|
||||
echo 'Methode nicht erlaubt.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$env = expand_env_values(parse_env_file(__DIR__ . '/../.env'));
|
||||
$pdo = connect_database($env);
|
||||
auth_bootstrap_session();
|
||||
auth_require_user($pdo);
|
||||
|
||||
$documentRoot = rtrim(env_value('HERMES_DOCUMENT_ROOT', $env), DIRECTORY_SEPARATOR);
|
||||
if ($documentRoot === '' || !str_starts_with($documentRoot, DIRECTORY_SEPARATOR)) {
|
||||
http_response_code(500);
|
||||
echo 'Dokumentablage ist nicht konfiguriert.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$resolvedRoot = realpath($documentRoot);
|
||||
$relativePath = (string) ($_GET['path'] ?? '');
|
||||
|
||||
if ($resolvedRoot === false || !is_dir($resolvedRoot) || $relativePath === '' || str_contains($relativePath, "\0") || str_contains($relativePath, '\\') || str_starts_with($relativePath, '/')) {
|
||||
http_response_code(404);
|
||||
echo 'Dokument nicht gefunden.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$segments = explode('/', $relativePath);
|
||||
foreach ($segments as $segment) {
|
||||
if ($segment === '' || $segment === '.' || $segment === '..') {
|
||||
http_response_code(404);
|
||||
echo 'Dokument nicht gefunden.';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$resolvedFile = realpath($resolvedRoot . DIRECTORY_SEPARATOR . $relativePath);
|
||||
$rootPrefix = $resolvedRoot . DIRECTORY_SEPARATOR;
|
||||
if ($resolvedFile === false || !is_file($resolvedFile) || !is_readable($resolvedFile) || !str_starts_with($resolvedFile, $rootPrefix)) {
|
||||
http_response_code(404);
|
||||
echo 'Dokument nicht gefunden.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$fileSize = filesize($resolvedFile);
|
||||
$mimeType = mime_content_type($resolvedFile);
|
||||
if ($fileSize === false || $mimeType === false) {
|
||||
http_response_code(500);
|
||||
echo 'Dokument konnte nicht bereitgestellt werden.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$downloadName = basename($resolvedFile);
|
||||
header('Content-Type: ' . $mimeType);
|
||||
header('Content-Length: ' . (string) $fileSize);
|
||||
header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($downloadName));
|
||||
header('Cache-Control: private, no-store');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
readfile($resolvedFile);
|
||||
+13
-1
@@ -49,6 +49,7 @@ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
|
||||
$currentUser = auth_current_user($pdo);
|
||||
if ($currentUser !== null) {
|
||||
$otcProducts = get_otc_order_form_products($pdo);
|
||||
$otcMarketOptions = market_management_list_active_occurrence_options($pdo);
|
||||
$lagerProducts = get_lager_product_overview($pdo);
|
||||
$lagerManufacturers = get_manufacturer_options($pdo);
|
||||
$lagerArtikel = get_lager_artikel_overview($pdo);
|
||||
@@ -78,6 +79,17 @@ if ($currentUser !== null) {
|
||||
'page_size' => 20,
|
||||
];
|
||||
$marketOccurrencesTable['has_more'] = count($marketOccurrencesTable['rows']) >= $marketLimit;
|
||||
$marketDefinitionsTable = [
|
||||
'rows' => market_management_search_markets($pdo, [
|
||||
'include_archived' => false,
|
||||
'sort' => 'name',
|
||||
'sort_direction' => 'ASC',
|
||||
'limit' => 100,
|
||||
'offset' => 0,
|
||||
]),
|
||||
'categories' => market_management_list_categories($pdo),
|
||||
'organizers' => market_management_list_organizers($pdo),
|
||||
];
|
||||
if ((string) ($_GET['bestellungen_fragment'] ?? '') === '1') {
|
||||
echo auth_render_bestellungen_content($bestellungenTable);
|
||||
exit;
|
||||
@@ -86,7 +98,7 @@ if ($currentUser !== null) {
|
||||
echo auth_render_markets_content($marketOccurrencesTable);
|
||||
exit;
|
||||
}
|
||||
render_auth_home_page($currentUser, $otcProducts, $bestellungenTable, $marketOccurrencesTable, $lagerProducts, $lagerArtikel, $lagerManufacturers);
|
||||
render_auth_home_page($currentUser, $otcProducts, $otcMarketOptions, $bestellungenTable, $marketOccurrencesTable, $lagerProducts, $lagerArtikel, $lagerManufacturers, $marketDefinitionsTable);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
|
||||
const getCard = (element) => element.closest(cardSelector);
|
||||
|
||||
const getDirectDeleteElement = (card, selector) => Array.from(card?.children || [])
|
||||
.find((child) => child.matches(selector)) || null;
|
||||
|
||||
const setEditMode = (card, enabled) => {
|
||||
if (!card) {
|
||||
return;
|
||||
@@ -12,6 +15,7 @@
|
||||
|
||||
const readView = card.querySelector('[data-shared-editable-card-read]');
|
||||
const editView = card.querySelector('[data-shared-editable-card-form]');
|
||||
const editButtons = card.querySelectorAll('[data-shared-editable-card-edit]');
|
||||
|
||||
if (readView) {
|
||||
readView.hidden = enabled;
|
||||
@@ -19,12 +23,18 @@
|
||||
if (editView) {
|
||||
editView.hidden = !enabled;
|
||||
}
|
||||
editButtons.forEach((editButton) => {
|
||||
editButton.disabled = enabled;
|
||||
editButton.setAttribute('aria-disabled', String(enabled));
|
||||
editButton.classList.toggle('sg-button--inactive', enabled);
|
||||
});
|
||||
};
|
||||
|
||||
const resetDeleteConfirmation = (card) => {
|
||||
const confirmation = card?.querySelector('[data-shared-editable-card-delete-confirmation]');
|
||||
const input = card?.querySelector('[data-shared-editable-card-delete-input]');
|
||||
const submit = card?.querySelector('[data-shared-editable-card-delete-submit]');
|
||||
const stage = card?.closest('.sg-delete-confirmation-pattern__stage');
|
||||
const confirmation = getDirectDeleteElement(card, '[data-shared-editable-card-delete-confirmation]');
|
||||
const input = confirmation?.querySelector('[data-shared-editable-card-delete-input]');
|
||||
const submit = confirmation?.querySelector('[data-shared-editable-card-delete-submit]');
|
||||
|
||||
if (input) {
|
||||
input.value = '';
|
||||
@@ -37,14 +47,15 @@
|
||||
if (confirmation) {
|
||||
confirmation.hidden = true;
|
||||
}
|
||||
if (card) {
|
||||
card.dataset.dialogOpen = 'false';
|
||||
if (stage) {
|
||||
stage.dataset.dialogOpen = 'false';
|
||||
}
|
||||
};
|
||||
|
||||
const openDeleteConfirmation = (card) => {
|
||||
const confirmation = card?.querySelector('[data-shared-editable-card-delete-confirmation]');
|
||||
const input = card?.querySelector('[data-shared-editable-card-delete-input]');
|
||||
const stage = card?.closest('.sg-delete-confirmation-pattern__stage');
|
||||
const confirmation = getDirectDeleteElement(card, '[data-shared-editable-card-delete-confirmation]');
|
||||
const input = confirmation?.querySelector('[data-shared-editable-card-delete-input]');
|
||||
|
||||
if (input) {
|
||||
input.value = '';
|
||||
@@ -53,14 +64,15 @@
|
||||
if (confirmation) {
|
||||
confirmation.hidden = false;
|
||||
}
|
||||
if (card) {
|
||||
card.dataset.dialogOpen = 'true';
|
||||
if (stage) {
|
||||
stage.dataset.dialogOpen = 'true';
|
||||
}
|
||||
};
|
||||
|
||||
const updateDeleteButton = (input) => {
|
||||
const card = getCard(input);
|
||||
const submit = card?.querySelector('[data-shared-editable-card-delete-submit]');
|
||||
const confirmation = input?.closest('[data-shared-editable-card-delete-confirmation]');
|
||||
const submit = confirmation?.querySelector('[data-shared-editable-card-delete-submit]');
|
||||
const token = card?.dataset.sharedEditableDeleteToken || 'DELETE';
|
||||
const valid = input.value === token;
|
||||
|
||||
@@ -77,6 +89,7 @@
|
||||
}
|
||||
|
||||
root.dataset.sharedEditableActionCardsInitialized = 'true';
|
||||
root.querySelectorAll(cardSelector).forEach((card) => setEditMode(card, false));
|
||||
root.addEventListener('click', (event) => {
|
||||
const edit = event.target.closest('[data-shared-editable-card-edit]');
|
||||
if (edit && root.contains(edit)) {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const create = ({ contentRoot, bodyClass, hostClass, hostDataAttribute, drawerDataAttribute, backdropSelector, backMarkup, historyKey }) => {
|
||||
let host = null;
|
||||
let drawer = null;
|
||||
let trigger = null;
|
||||
let open = false;
|
||||
|
||||
const restore = () => {
|
||||
if (host && host.parentNode) {
|
||||
const mobileDrawer = host.querySelector('[' + drawerDataAttribute + ']');
|
||||
if (mobileDrawer) {
|
||||
contentRoot.appendChild(mobileDrawer);
|
||||
mobileDrawer.removeAttribute(drawerDataAttribute);
|
||||
mobileDrawer.dataset.open = 'false';
|
||||
mobileDrawer.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
host.parentNode.removeChild(host);
|
||||
}
|
||||
|
||||
if (trigger) {
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
const backdrops = backdropSelector
|
||||
? [contentRoot.querySelector(backdropSelector)].filter(Boolean)
|
||||
: Array.from(contentRoot.querySelectorAll('.sg-card-list-page-drawer-backdrop'));
|
||||
backdrops.forEach((backdrop) => {
|
||||
backdrop.dataset.open = 'false';
|
||||
backdrop.setAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
host = null;
|
||||
drawer = null;
|
||||
trigger = null;
|
||||
open = false;
|
||||
document.body.classList.remove(bodyClass);
|
||||
document.body.classList.remove('sg-card-list-page-drawer-open');
|
||||
};
|
||||
|
||||
const mount = (sourceDrawer, detailTrigger, existingHost = null) => {
|
||||
restore();
|
||||
if (!sourceDrawer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
host = existingHost || document.createElement('div');
|
||||
host.className = hostClass;
|
||||
host.setAttribute(hostDataAttribute, 'true');
|
||||
if (!existingHost) {
|
||||
host.innerHTML = backMarkup;
|
||||
}
|
||||
drawer = sourceDrawer;
|
||||
trigger = detailTrigger;
|
||||
drawer.setAttribute(drawerDataAttribute, 'true');
|
||||
drawer.dataset.open = 'true';
|
||||
drawer.setAttribute('aria-hidden', 'false');
|
||||
host.appendChild(drawer);
|
||||
document.body.appendChild(host);
|
||||
document.body.classList.add(bodyClass);
|
||||
open = true;
|
||||
|
||||
if (historyKey) {
|
||||
window.history.pushState({ ...(window.history.state || {}), [historyKey]: 'detail' }, '', window.location.href);
|
||||
}
|
||||
|
||||
return { host, drawer, trigger: detailTrigger };
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
if (window.history.state && window.history.state[historyKey] === 'detail') {
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
restore();
|
||||
};
|
||||
|
||||
return { mount, restore, close, isOpen: () => open, getHost: () => host, getDrawer: () => drawer };
|
||||
};
|
||||
|
||||
window.SharedMobileDetail = { create };
|
||||
}());
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../modules/market_management/mcp.php';
|
||||
|
||||
function fail_test(string $message): void
|
||||
{
|
||||
fwrite(STDERR, $message . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function assert_test(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
fail_test($message);
|
||||
}
|
||||
}
|
||||
|
||||
function assert_schema_objects(array $schema, string $path = 'inputSchema'): void
|
||||
{
|
||||
assert_test(($schema['type'] ?? null) === 'object', $path . '.type must be object');
|
||||
assert_test(isset($schema['properties']) && is_object($schema['properties']), $path . '.properties must be an object');
|
||||
|
||||
if (isset($schema['required'])) {
|
||||
assert_test(is_array($schema['required']), $path . '.required must be an array');
|
||||
foreach ($schema['required'] as $required) {
|
||||
assert_test(is_string($required), $path . '.required entries must be strings');
|
||||
}
|
||||
}
|
||||
|
||||
foreach (get_object_vars($schema['properties']) as $propertyName => $propertySchema) {
|
||||
assert_test(is_array($propertySchema), $path . '.properties.' . $propertyName . ' must be an object schema');
|
||||
if (($propertySchema['type'] ?? null) === 'array' && isset($propertySchema['items'])) {
|
||||
assert_test(is_array($propertySchema['items']), $path . '.properties.' . $propertyName . '.items must be an object schema');
|
||||
if (($propertySchema['items']['type'] ?? null) === 'object') {
|
||||
assert_schema_objects($propertySchema['items'], $path . '.properties.' . $propertyName . '.items');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$expected = [
|
||||
'market_management.search_markets' => 'Märkte suchen',
|
||||
'market_management.search_occurrences' => 'Marktdurchführungen suchen und auflisten',
|
||||
'market_management.get_market' => 'Markt mit Marktdurchführungen lesen',
|
||||
'market_management.create_market' => 'Markt mit verpflichtendem Veranstalter, Kategorie und optionaler Website-URL anlegen',
|
||||
'market_management.update_market' => 'Markt inklusive Veranstalter und Website-URL ändern',
|
||||
'market_management.archive_market' => 'Markt archivieren',
|
||||
'market_management.reactivate_market' => 'Markt reaktivieren',
|
||||
'market_management.list_categories' => 'Bestehende Kategorien lesen',
|
||||
'market_management.list_market_organizers' => 'Aktive Veranstalter lesen',
|
||||
'market_management.create_market_organizer' => 'Veranstalter mit optionalen Kontaktdaten anlegen',
|
||||
'market_management.list_reference_data' => 'Kategorien, Kantone und Gemeinden lesen',
|
||||
'market_management.create_municipality' => 'Lokale Gemeinde im Markt-Gemeinderegister anlegen',
|
||||
'market_management.get_overview' => 'Marktübersicht mit serverseitigen Filtern lesen',
|
||||
'market_management.create_occurrence' => 'Marktdurchführung mit mindestens einem Markttag anlegen',
|
||||
'market_management.get_occurrence' => 'Marktdurchführung mit Markt, Markttagen und berechneten Verkaufsdaten lesen',
|
||||
'market_management.update_occurrence' => 'Marktdurchführung ändern',
|
||||
'market_management.archive_occurrence' => 'Marktdurchführung archivieren',
|
||||
'market_management.reactivate_occurrence' => 'Marktdurchführung reaktivieren',
|
||||
'market_management.create_day' => 'Markttag anlegen',
|
||||
'market_management.update_day' => 'Markttag ändern',
|
||||
'market_management.delete_day' => 'Markttag entfernen',
|
||||
];
|
||||
|
||||
$tools = market_management_mcp_tools();
|
||||
assert_test(count($tools) === 21, 'Expected 21 MCP tools, got ' . count($tools));
|
||||
|
||||
$actual = [];
|
||||
foreach ($tools as $tool) {
|
||||
assert_test(isset($tool['name'], $tool['description'], $tool['inputSchema']), 'Tool must contain name, description and inputSchema');
|
||||
assert_schema_objects($tool['inputSchema'], $tool['name'] . '.inputSchema');
|
||||
$actual[$tool['name']] = $tool['description'];
|
||||
}
|
||||
|
||||
assert_test($actual === $expected, 'MCP tool names or descriptions changed unexpectedly');
|
||||
fwrite(STDOUT, "market_management_mcp_tools_list_test: OK\n");
|
||||
Reference in new issue
Block a user