Compare commits

...
Author SHA1 Message Date
gitea_admin 3d7f6b1820 Add read-only Hermes document links 2026-09-21 15:00:04 +02:00
gitea_admin d0079aa5f7 Sync styleguide 2026.05.18.1 2026-09-21 14:42:55 +02:00
gitea_admin e2e6a40d9a Sync styleguide 2026.05.18.1 2026-09-21 14:28:47 +02:00
gitea_admin 99eae4cada Kostenpositionen auf ein Textfeld vereinheitlichen 2026-09-21 12:55:41 +02:00
gitea_admin de10e37a0c GUI Kostenpositionen nach dem Speichern anzeigen 2026-09-21 12:09:57 +02:00
gitea_admin 4758aecec2 MCP-Doku um Kostenpositionen-Rückgabe ergänzen 2026-09-21 11:51:18 +02:00
gitea_admin c4e1c291d8 MCP Kostenpositionen als Text ausgeben 2026-09-21 11:49:09 +02:00
gitea_admin 42127b0916 Verkaufszahl um Umsatz erweitern 2026-09-21 08:21:02 +02:00
gitea_admin d03f653848 Verkaufszahl als eigene Ganzzahlspalte darstellen 2026-09-21 08:16:26 +02:00
gitea_admin b1fd16cf06 Verkaufte Produkte in Durchführungen anzeigen 2026-09-20 21:47:19 +02:00
gitea_admin b2cb88e4ff Archivierte Märkte aus aktiver Liste ausblenden 2026-09-20 21:43:23 +02:00
gitea_admin ee9bcc85ce Archivierten Markt aus aktiver Liste entfernen 2026-09-20 21:39:18 +02:00
gitea_admin 4f0094a821 Marktformular-Fehler verständlich und rot anzeigen 2026-09-20 21:07:52 +02:00
gitea_admin c2913c1026 MCP-Booleans typisiert an PostgreSQL binden 2026-09-20 21:00:26 +02:00
gitea_admin 13420b0e01 Marktdurchführung-Löschung korrekt ausführen 2026-09-20 20:53:11 +02:00
gitea_admin 5567b53581 Delete-Handler mit Asset-Version ausliefern 2026-09-20 20:48:23 +02:00
gitea_admin ff0fffccad Verschachtelte Delete-Dialoge korrekt zuordnen 2026-09-20 20:46:53 +02:00
gitea_admin 38886e39bd CSRF-Token bei Markt-Fragmenten erhalten 2026-09-20 20:35:05 +02:00
gitea_admin 25ef8ef7b0 MCP Occurrence-Felder beim Schreiben übernehmen 2026-09-20 19:45:55 +02:00
gitea_admin 6750d4778f Gespeicherte Portalnavigation beim Reload wiederherstellen 2026-09-20 13:18:26 +02:00
gitea_admin 7fc75eaee6 Anmeldefrist in Marktdurchführungstabelle anzeigen 2026-09-20 13:14:53 +02:00
gitea_admin f1b090ee85 Portalnavigation nach Reload beibehalten 2026-09-20 13:13:39 +02:00
gitea_admin e6431dbe66 Tabelle nach Durchführungsänderung aktualisieren 2026-09-20 13:12:10 +02:00
gitea_admin 23c0cf69d7 Mehrzeilige Marktformularfelder auf Einzeilenbreite setzen 2026-09-20 13:07:52 +02:00
gitea_admin e4be458433 Bezahlten Betrag unter Steuern anordnen 2026-09-20 13:05:51 +02:00
gitea_admin 0c8cb05237 Bezahlten Betrag im Durchführungsformular editierbar machen 2026-09-20 13:01:13 +02:00
gitea_admin 5d9c058e10 Kostenwerte bei Steuerstatuswechsel sofort aktualisieren 2026-09-20 12:59:08 +02:00
gitea_admin af235987ea Steuerstatus und Steuersatz in Kostenkarte wiederherstellen 2026-09-20 12:57:09 +02:00
gitea_admin bae293ad94 Drawer durch Kostenfeld-Observer nicht blockieren 2026-09-20 12:52:29 +02:00
gitea_admin 1ad35cdcfd Kostenpositionen durch einfache Gesamtkosten ersetzen 2026-09-20 12:47:37 +02:00
gitea_admin c0c3c2600f Kostenpositions-Overlay ohne zusaetzlichen Observer binden 2026-09-20 12:40:06 +02:00
gitea_admin 5413bae3ea Kostenpositionsloeschung mit Overlay-Card bestaetigen 2026-09-20 12:35:37 +02:00
gitea_admin e51f3ab507 Kostenpositions-Hinzufuegen rechtsbuendig ausrichten 2026-09-20 12:31:41 +02:00
gitea_admin e1f482ef5c Zusatzabstand vor Kostenpositionsloeschbutton entfernen 2026-09-20 12:30:32 +02:00
gitea_admin f14023bd92 Abstand des Kostenpositions-Löschbuttons reduzieren 2026-09-20 12:28:19 +02:00
gitea_admin d68654e94f Kostenpositionsueberschrift im Drawer bereinigen 2026-09-20 12:25:56 +02:00
gitea_admin 724e3413e4 Kostenpositions-Löschbutton im Formular ausrichten 2026-09-20 12:24:08 +02:00
gitea_admin f77ce71bd6 Kostenpositionsspeicherung atomar ausführen 2026-09-20 12:20:57 +02:00
gitea_admin dc52003c01 Standkosten mit Standardsteuersatz versehen 2026-09-20 12:19:13 +02:00
gitea_admin 3a0cb81a97 Kostenarten und geschützte Standkosten umsetzen 2026-09-20 12:16:31 +02:00
gitea_admin 313475afec Kostenpositionen als Formular-Kapitel speichern 2026-09-20 12:06:20 +02:00
gitea_admin 6226f2a037 Kostenpositionen im Durchführungsdrawer bearbeiten 2026-09-20 11:58:25 +02:00
gitea_admin 2a09401600 Gemeindenamen im Durchführungsdrawer anzeigen 2026-09-20 11:50:50 +02:00
gitea_admin 2d3c9ce4ab Gemeinderegister für Marktdurchführungen ergänzen 2026-09-20 11:48:00 +02:00
gitea_admin 273fb05833 Align historical lots and market drawer 2026-09-20 11:39:25 +02:00
gitea_admin dfe9748ff9 Render drawer details in two columns 2026-09-20 11:35:31 +02:00
gitea_admin 1b66cd3add Neue Veranstalterfelder standardmässig ausblenden 2026-09-20 11:13:54 +02:00
gitea_admin a6f9236abf Geerbten Veranstalter im Editmodus anzeigen 2026-09-20 11:05:39 +02:00
gitea_admin 36fb17ac32 Veranstalter strikt von Lieferanten trennen 2026-09-20 11:04:13 +02:00
gitea_admin 0e71aca89b Veranstalter am Markt verankern 2026-09-20 10:57:40 +02:00
gitea_admin ba36485180 Align market drawer edit action 2026-09-20 10:38:30 +02:00
gitea_admin 0edd9e7b3c Use compatible market drawer script output 2026-09-20 10:35:57 +02:00
gitea_admin 1fcd6aa245 Fix market drawer script syntax 2026-09-20 10:35:37 +02:00
gitea_admin 1d0e71c5db Keep market drawer actions in card header 2026-09-20 10:35:16 +02:00
gitea_admin 5825d37e83 Fix market drawer edit action layout 2026-09-20 10:30:58 +02:00
gitea_admin 5b83d10d92 Fix market status text contrast 2026-09-20 10:21:46 +02:00
gitea_admin 95c42b342b Make occurrence market fields read-only 2026-09-20 10:19:42 +02:00
gitea_admin 78cc6390f7 Add Markt-Drawer delete card 2026-09-20 10:13:23 +02:00
gitea_admin 9b49db98d4 Restore normal Markt-Link weight 2026-09-20 10:06:24 +02:00
gitea_admin e6bf7be2f9 Fix Markt-Link Cursor 2026-09-20 10:05:00 +02:00
gitea_admin 212476c3f6 Fix Markt-Detaildrawer 2026-09-20 10:01:27 +02:00
gitea_admin 42684f3864 Marktname als Drawer-Aktion rendern 2026-09-20 09:50:35 +02:00
gitea_admin e2e46a601b Marktlink im dynamischen Tabelleninhalt robust öffnen 2026-09-20 09:46:15 +02:00
gitea_admin a1ac5f34ae Marktlinks direkt an Detaildrawer binden 2026-09-20 09:42:06 +02:00
gitea_admin 1bf5b51679 Markt-Navigation wieder sichtbar schalten 2026-09-20 09:39:50 +02:00
gitea_admin b68546f824 Markt-Detailkarte an zentrales Editierpattern anbinden 2026-09-20 09:34:16 +02:00
gitea_admin 841e559a70 Markt-Bearbeiten robust gegen fehlenden Formulartitel machen 2026-09-20 09:30:06 +02:00
gitea_admin f0af2efe8a Markt-Bearbeiten direkt an Detailkarte binden 2026-09-20 09:17:25 +02:00
gitea_admin c9d386badd Marktname im Tabellen-Link normal gewichten 2026-09-20 09:14:57 +02:00
gitea_admin f4834e0e7a Markt-Drawer zeigt keine Neuanlagekarte in Details 2026-09-20 09:13:35 +02:00
gitea_admin 1921464e27 Marktname öffnet Detailkarte mit Lese- und Editieransicht 2026-09-20 09:07:19 +02:00
gitea_admin f720782736 Markt-Neuanlage im Märkte-Drawer implementieren 2026-09-20 08:57:54 +02:00
gitea_admin 43598bf65a MCP-Begriffe für Markt-Hierarchie und LLM-Doku vereinheitlichen 2026-09-20 08:49:05 +02:00
gitea_admin 2bfe91695e Marktbegriffe in Durchführungen und OTC vereinheitlichen 2026-09-20 08:46:17 +02:00
gitea_admin 22880b512a Marktdefinitionen als eigene Märkte-Konfiguration anzeigen 2026-09-20 08:34:08 +02:00
gitea_admin debee672f8 Märkte-Navigation in Markttage und Märkte umbenennen 2026-09-20 08:30:10 +02:00
gitea_admin 3d60998145 MCP-Toolschemas vollständig beschreiben 2026-09-19 21:14:34 +02:00
gitea_admin 481da014bc MCP-API an GUI-Felder und Durchführungsdaten angleichen 2026-09-19 21:13:43 +02:00
gitea_admin d22395b96c Bestätigungscard im zentralen Pattern hellgrau rendern 2026-09-19 21:05:28 +02:00
gitea_admin aaa9fa67e9 Bestätigungskarten zentral hellgrau darstellen 2026-09-19 21:04:10 +02:00
gitea_admin ca2facdbc6 Tages-Löschdialog auf die Durchführungskarte begrenzen 2026-09-19 21:03:23 +02:00
gitea_admin 3d638ab2a1 Durchführungstag im bestehenden Löschpattern entfernen 2026-09-19 20:59:53 +02:00
gitea_admin 30c11c5ca7 Durchführungsstatus im Tagescron automatisch abschliessen 2026-09-19 20:50:00 +02:00
gitea_admin 2a1144f0aa Tagesspezifisch aus Durchführungskarten entfernen 2026-09-19 20:37:12 +02:00
gitea_admin 21e0f48fd5 OTC-Response vor Throttling-Warnung schützen 2026-09-19 20:30:15 +02:00
gitea_admin 9fc85141e0 OTC-Veranstaltungen eindeutig verknüpfen und Fehler sichtbar halten 2026-09-19 20:21:52 +02:00
gitea_admin 184f50d034 Spacing zwischen Neuer-Durchführung-Karte und Tabelle korrigieren 2026-09-19 19:59:18 +02:00
gitea_admin 6fae2de15f Neue Durchführungen im Märkte-Drawer anlegen 2026-09-19 19:49:30 +02:00
gitea_admin e188f932f4 Remove market edit form headings 2026-09-19 18:33:11 +02:00
gitea_admin 71f329170f Align editable cards with read-only fields 2026-09-19 18:29:32 +02:00
gitea_admin 3ba33f85a0 Aggregate OTC sales by market day 2026-09-19 17:42:42 +02:00
gitea_admin 1b553a9095 Separate OTC market link segment 2026-09-19 17:37:10 +02:00
gitea_admin e9d157c87e Use disabled pulldown style for market link 2026-09-19 17:33:56 +02:00
gitea_admin 47c09a3e4c Link OTC sales to market days 2026-09-19 17:32:23 +02:00
gitea_admin 759e337976 Place observations before inheritance checkbox 2026-09-19 17:17:26 +02:00
gitea_admin b15eda6ff1 Place observations below day date 2026-09-19 17:15:09 +02:00
gitea_admin 71b27a387d Keep day observations independent 2026-09-19 17:10:25 +02:00
gitea_admin 2c734bd415 Fix Tag-1-Vererbung Checkbox 2026-09-19 16:59:32 +02:00
gitea_admin 20f130f614 Add first-day inheritance for occurrence days 2026-09-19 16:50:53 +02:00
gitea_admin 4eef9d9785 Validate chronological market occurrence days 2026-09-19 16:38:32 +02:00
gitea_admin 361635332c Apply highlighted market day tab style 2026-09-19 16:32:56 +02:00
gitea_admin a42d7a0718 Fix new market day tab selection 2026-09-19 16:27:00 +02:00
gitea_admin 4b112f3a57 Add market occurrence day tab creation 2026-09-19 16:24:45 +02:00
gitea_admin 0de04a8dfa Mark required market drawer fields 2026-09-19 16:09:39 +02:00
gitea_admin bace7b4a05 Tune occurrence days heading spacing 2026-09-19 16:01:24 +02:00
gitea_admin 18e89595e0 Add heading for market occurrence days 2026-09-19 16:00:08 +02:00
gitea_admin d1129175bc Fix delete confirmation backdrop state 2026-09-19 15:57:25 +02:00
gitea_admin c1cb272702 Move market delete action into titled card 2026-09-19 15:55:03 +02:00
gitea_admin b7ff51adc5 Show occurrence dates in market drawer title 2026-09-19 15:50:42 +02:00
gitea_admin fdc0f02b91 Restore market drawer card spacing 2026-09-19 15:48:44 +02:00
gitea_admin c4b93ff217 Align market delete confirmation overlay 2026-09-19 15:42:17 +02:00
gitea_admin ca42ded4e2 Add market occurrence delete confirmation 2026-09-19 15:39:05 +02:00
gitea_admin 30e44755fe Allow empty market municipality 2026-09-19 14:28:52 +02:00
gitea_admin 2862d7a6e9 Allow saving without contact details 2026-09-19 14:28:02 +02:00
gitea_admin 28784ed6ac Fix market card save validation 2026-09-19 14:26:50 +02:00
gitea_admin 6a12a43114 Complete market overview card editing 2026-09-19 14:22:07 +02:00
gitea_admin 94d336f090 Complete market overview edit fields 2026-09-19 14:12:00 +02:00
gitea_admin af0206d9cd Keep market status header visible 2026-09-19 14:09:15 +02:00
gitea_admin 65d2b1405f Remove cache workaround 2026-09-19 14:05:52 +02:00
gitea_admin 57d8da9577 Refresh shared editable card script 2026-09-19 14:05:17 +02:00
gitea_admin 2a754973cd Disable edit action while card is open 2026-09-19 14:04:16 +02:00
gitea_admin e503759197 Restore market drawer loader 2026-09-19 13:53:46 +02:00
gitea_admin 47baeaa5f7 Remove unused market edit legacy 2026-09-19 13:51:50 +02:00
gitea_admin 0fba652770 Remove legacy market edit path 2026-09-19 13:51:35 +02:00
gitea_admin 9064d4eb42 Fix market status card edit mode 2026-09-19 13:49:41 +02:00
gitea_admin f66c22027e Restore portal navigation JavaScript 2026-09-19 13:42:07 +02:00
gitea_admin 614a934dc1 Fix portal JavaScript syntax 2026-09-19 13:37:56 +02:00
gitea_admin 906c76a959 Align market cards with shared edit pattern 2026-09-19 13:36:01 +02:00
gitea_admin f2467f4411 Use shared editable card pattern for markets 2026-09-19 13:26:03 +02:00
gitea_admin aa68b41971 Add market drawer edit patterns 2026-09-19 13:17:22 +02:00
gitea_admin fdabe38180 Improve neutral market status contrast 2026-09-19 13:13:49 +02:00
gitea_admin ecdd973c3f Simplify market status card 2026-09-19 13:08:40 +02:00
gitea_admin fe641e9e7c Remove market drawer edit actions 2026-09-19 13:06:56 +02:00
gitea_admin 0c3a87868c Refine market drawer detail presentation 2026-09-19 13:02:43 +02:00
gitea_admin 278e415e08 Complete market occurrence drawer details 2026-09-19 12:59:14 +02:00
gitea_admin 16882207bb Merge shared editable action card feature 2026-09-19 12:40:02 +02:00
36 changed files with 2649 additions and 271 deletions

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;
+28
View File
@@ -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;
+63
View File
@@ -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,52 @@ 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 vollständige
HTTPS-URL in das Feld `notes` aufnehmen. Der bestehende Text darf erhalten
bleiben; die Position im Freitext ist frei wählbar.
Die URL verwendet dieses Format:
```text
https://erpnaurua.imhochrain.ch/files.php?path=<RFC-3986-url-encodierter-relativer-Pfad>
```
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: https://erpnaurua.imhochrain.ch/files.php?path=Marktmanagement%2F2026%20Bellevue%20No%C3%ABl%2FStandmietvertrag_Naurua.pdf
```
Leerzeichen, Umlaute und sonstige Sonderzeichen müssen vollständig URL-encodiert
werden. Die ERP-Anzeige macht ausschließlich diese Dokument-URLs klickbar. Der
Link ist schreibgeschützt und erfordert eine aktive ERP-Anmeldung.
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 +118,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 +150,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 +196,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 +212,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 +247,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 +297,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,10 @@ 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 vollständige HTTPS-URL
im Feld `notes` referenziert werden. Die Position im bestehenden Freitext ist
frei wählbar.
- 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
@@ -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
@@ -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">
+164
View File
@@ -0,0 +1,164 @@
(function initTooltipOverlayModule() {
const getViewportWidth = () => {
if (window.visualViewport && typeof window.visualViewport.width === 'number') {
return window.visualViewport.width;
}
return window.innerWidth;
};
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 setTooltipState = (wrap, isOpen) => {
const trigger = wrap.querySelector('[data-component-part="tooltip-trigger"]');
const panel = wrap.querySelector('[data-component-part="tooltip-panel"]');
if (!trigger || !panel) {
return;
}
wrap.dataset.tooltipOpen = String(isOpen);
panel.hidden = !isOpen;
trigger.setAttribute('aria-expanded', String(isOpen));
};
const positionTooltip = (wrap) => {
const trigger = wrap.querySelector('[data-component-part="tooltip-trigger"]');
const panel = wrap.querySelector('[data-component-part="tooltip-panel"]');
if (!trigger || !panel || panel.hidden) {
return;
}
const viewportWidth = getViewportWidth();
const safeInset = getPixelValue('--spacing-large');
const triggerRect = trigger.getBoundingClientRect();
const panelRect = panel.getBoundingClientRect();
const gap = getPixelValue('--spacing-small');
let placement = 'bottom';
if (triggerRect.right + (panelRect.width / 2) > viewportWidth - safeInset) {
placement = 'left';
} else if (triggerRect.left - (panelRect.width / 2) < safeInset) {
placement = 'right';
}
wrap.dataset.tooltipPlacement = placement;
if (placement === 'bottom') {
const nextPanelRect = panel.getBoundingClientRect();
if (nextPanelRect.bottom > window.innerHeight - safeInset
&& triggerRect.top - panelRect.height - gap >= safeInset) {
wrap.dataset.tooltipPlacement = 'top';
}
}
};
const closeTooltip = (wrap) => {
wrap.dataset.tooltipPinned = 'false';
wrap.dataset.tooltipHover = 'false';
wrap.dataset.tooltipFocus = 'false';
wrap.dataset.tooltipDismissed = 'false';
setTooltipState(wrap, false);
};
const openTooltip = (wrap) => {
setTooltipState(wrap, true);
positionTooltip(wrap);
};
const initTooltip = (wrap) => {
if (wrap.dataset.tooltipInit === 'true') {
return;
}
wrap.dataset.tooltipInit = 'true';
wrap.dataset.tooltipPlacement = 'bottom';
wrap.dataset.tooltipPinned = 'false';
wrap.dataset.tooltipHover = 'false';
wrap.dataset.tooltipFocus = 'false';
wrap.dataset.tooltipDismissed = 'false';
setTooltipState(wrap, false);
const trigger = wrap.querySelector('[data-component-part="tooltip-trigger"]');
if (!trigger) {
return;
}
wrap.addEventListener('pointerenter', () => {
if (wrap.dataset.tooltipDismissed === 'true') {
return;
}
wrap.dataset.tooltipHover = 'true';
openTooltip(wrap);
});
wrap.addEventListener('pointerleave', () => {
wrap.dataset.tooltipHover = 'false';
wrap.dataset.tooltipDismissed = 'false';
if (wrap.dataset.tooltipPinned !== 'true' && wrap.dataset.tooltipFocus !== 'true') {
setTooltipState(wrap, false);
}
});
wrap.addEventListener('focusin', () => {
if (wrap.dataset.tooltipDismissed === 'true') {
return;
}
wrap.dataset.tooltipFocus = 'true';
openTooltip(wrap);
});
wrap.addEventListener('focusout', (event) => {
if (wrap.contains(event.relatedTarget)) {
return;
}
wrap.dataset.tooltipFocus = 'false';
wrap.dataset.tooltipDismissed = 'false';
if (wrap.dataset.tooltipPinned !== 'true' && wrap.dataset.tooltipHover !== 'true') {
setTooltipState(wrap, false);
}
});
trigger.addEventListener('click', (event) => {
event.stopPropagation();
const isPinned = wrap.dataset.tooltipPinned === 'true';
wrap.dataset.tooltipPinned = String(!isPinned);
if (isPinned) {
wrap.dataset.tooltipDismissed = 'true';
setTooltipState(wrap, false);
return;
}
wrap.dataset.tooltipDismissed = 'false';
openTooltip(wrap);
});
};
document.querySelectorAll('.sg-tooltip-wrap').forEach(initTooltip);
document.addEventListener('click', (event) => {
document.querySelectorAll('.sg-tooltip-wrap').forEach((wrap) => {
if (!wrap.contains(event.target)) {
closeTooltip(wrap);
}
});
});
const repositionOpenTooltips = () => {
document.querySelectorAll('.sg-tooltip-wrap[data-tooltip-open="true"]').forEach(positionTooltip);
};
window.addEventListener('resize', repositionOpenTooltips);
window.addEventListener('scroll', repositionOpenTooltips, true);
})();
@@ -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>
@@ -62,6 +62,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 +90,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 +101,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");
@@ -1239,6 +1239,95 @@
right: 0;
}
.sg-tooltip-wrap {
position: relative;
display: inline-flex;
vertical-align: middle;
}
.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-pulldown-panel {
position: absolute;
top: calc(100% + var(--spacing-small));
+6 -2
View File
@@ -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();
+47 -1
View File
@@ -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();
}
+95
View File
@@ -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);
+150 -47
View File
@@ -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' => []]],
['name' => 'market_management.list_market_organizers', 'description' => 'Aktive Veranstalter lesen', 'inputSchema' => ['type' => 'object', 'properties' => []]],
['name' => 'market_management.create_market_organizer', 'description' => 'Veranstalter mit optionalen Kontaktdaten anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name'], 'properties' => ['name' => ['type' => 'string'], 'email' => ['type' => ['string', 'null'], 'format' => 'email'], 'phone' => ['type' => ['string', 'null']]]]],
['name' => 'market_management.list_reference_data', 'description' => 'Kategorien, Kantone und Gemeinden lesen', 'inputSchema' => ['type' => 'object', 'properties' => []]],
['name' => 'market_management.create_municipality', 'description' => 'Lokale Gemeinde im Markt-Gemeinderegister anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name'], 'properties' => ['name' => ['type' => 'string']]]],
['name' => 'market_management.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
+339 -75
View File
@@ -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,7 @@ 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>';
echo 'const portalModuleNavigation = ' . $moduleNavigation . ';';
echo 'const portalModuleContentCards = ' . $moduleContentCards . ';';
@@ -832,19 +939,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 +964,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 +973,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 +993,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 "});";
@@ -1026,6 +1142,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,17 +1152,30 @@ 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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[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 linked = escaped.replace(/https:\\/\\/erpnaurua\\.imhochrain\\.ch\\/files\\.php\\?path=[^\\s<]+/g, (url) => '<a class=\"sg-hyperlink\" href=\"' + url + '\" target=\"_blank\" rel=\"noopener\">' + url + '</a>'); return linked.replace(/\\r?\\n/g, '<br>'); };";
echo " const 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(); };";
@@ -1053,22 +1183,138 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
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 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 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 = () => { 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 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 <<<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) => { drawer.dataset.open = String(open); drawer.setAttribute('aria-hidden', String(!open)); if (activeMarket?.trigger) { activeMarket.trigger.setAttribute('aria-expanded', String(open)); } const backdrop = definitionContent.querySelector('[data-market-definition-drawer-backdrop]'); if (backdrop) { backdrop.dataset.open = String(open); backdrop.setAttribute('aria-hidden', String(!open)); } document.body.classList.toggle('sg-card-list-page-drawer-open', open); };";
echo " const resetForm = () => { form.reset(); const status = form.querySelector('[data-market-definition-status]'); if (status) { status.textContent = ''; } syncNewOrganizerFields(); };";
echo " const syncNewOrganizerFields = () => { const select = form.querySelector('[data-market-definition-organizer]'); const fields = form.querySelector('[data-market-definition-new-organizer]'); if (!select || !fields) { return; } fields.hidden = select.value !== '__new__'; };";
echo " const closeDrawer = () => { resetForm(); if (sharedCard && window.SharedEditableActionCard) { window.SharedEditableActionCard.resetDeleteConfirmation(sharedCard); window.SharedEditableActionCard.setEditMode(sharedCard, false); } activeMarket = null; setDrawerState(false); };";
echo " const deleteMarketDefinition = async (submit) => { if (submit.disabled || !activeMarket?.id) { return; } const card = submit.closest('[data-shared-editable-action-card]'); const input = card?.querySelector('[data-shared-editable-card-delete-input]'); const status = card?.querySelector('[data-market-definition-delete-status]'); if (!card || !input || input.value !== 'DELETE') { return; } if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); try { const response = await fetch('/api/market-definition.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': definitionContent.dataset.marketDefinitionsCsrfToken || '' }, body: JSON.stringify({ action: 'delete', id: activeMarket.id, confirmation: input.value }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Löschen fehlgeschlagen.'); } const row = activeMarket.trigger.closest('[data-market-definition-row]'); row?.remove(); closeDrawer(); } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Löschen fehlgeschlagen.'; } submit.disabled = false; submit.setAttribute('aria-disabled', 'false'); } };";
echo " const showReadCard = (trigger) => { activeMarket = { id: trigger.dataset.marketDefinitionId || '', trigger }; drawer.querySelector('[data-market-definition-read-title]').textContent = trigger.dataset.marketDefinitionName || 'Markt'; readCard.querySelector('[data-market-definition-read-name]').textContent = trigger.dataset.marketDefinitionName || 'Nicht hinterlegt'; readCard.querySelector('[data-market-definition-read-category]').textContent = trigger.dataset.marketDefinitionCategoryName || 'Nicht hinterlegt'; readCard.querySelector('[data-market-definition-read-organizer]').textContent = trigger.dataset.marketDefinitionOrganizerName || 'Nicht hinterlegt'; readCard.querySelector('[data-market-definition-read-website]').textContent = trigger.dataset.marketDefinitionWebsite || 'Nicht hinterlegt'; const title = form.querySelector('[data-market-definition-form-title]'); if (title) { title.textContent = 'Markt bearbeiten'; } const name = form.querySelector('[data-market-definition-name]'); const category = form.querySelector('[data-market-definition-category]'); const organizer = form.querySelector('[data-market-definition-organizer]'); const website = form.querySelector('[data-market-definition-website]'); if (name) { name.value = trigger.dataset.marketDefinitionName || ''; } if (category) { category.value = trigger.dataset.marketDefinitionCategoryId || ''; } if (organizer) { organizer.value = trigger.dataset.marketDefinitionOrganizerId || ''; } if (website) { website.value = trigger.dataset.marketDefinitionWebsite === 'Nicht hinterlegt' ? '' : (trigger.dataset.marketDefinitionWebsite || ''); } syncNewOrganizerFields(); if (sharedCard && window.SharedEditableActionCard) { window.SharedEditableActionCard.setEditMode(sharedCard, false); } setDrawerState(true); };";
echo " definitionContent.addEventListener('click', (event) => { const trigger = event.target.closest('[data-market-definition-open]'); if (!trigger || !definitionContent.contains(trigger)) { return; } event.preventDefault(); event.stopPropagation(); showReadCard(trigger); }, true);";
echo " definitionContent.addEventListener('click', (event) => { const 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 " 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 +1395,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 +1655,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 +1669,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 +1677,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 +1714,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 +1729,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 +1738,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 +1756,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 +1824,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 +1849,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 +1874,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 +1891,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 +1900,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>';
+1 -1
View File
@@ -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;
}
+2
View File
@@ -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) {
+84
View File
@@ -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);
}
+80 -10
View File
@@ -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;
}
+93
View File
@@ -62,6 +62,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 +90,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 +101,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");
@@ -1760,6 +1764,95 @@ section + section {
right: 0;
}
.sg-tooltip-wrap {
position: relative;
display: inline-flex;
vertical-align: middle;
}
.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-pulldown-panel {
position: absolute;
top: calc(100% + var(--spacing-small));
+2 -2
View File
@@ -1,7 +1,7 @@
{
"styleguideVersion": "2026.05.18.1",
"styleguideCommit": "71734d6",
"syncedAtUtc": "2026-09-08T09:49:27Z",
"styleguideCommit": "944d791",
"syncedAtUtc": "2026-09-21T12:42:55Z",
"sourceRepo": "/Users/mathias/Documents/Dokumente Chouchou/Codebases/Styleguide",
"mirroredDocsPath": "docs/styleguide"
}
+93
View File
@@ -62,6 +62,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 +90,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 +101,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");
@@ -1760,6 +1764,95 @@ section + section {
right: 0;
}
.sg-tooltip-wrap {
position: relative;
display: inline-flex;
vertical-align: middle;
}
.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-pulldown-panel {
position: absolute;
top: calc(100% + var(--spacing-small));
+61 -6
View File
@@ -84,22 +84,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 +315,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 +360,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;
}
+67
View File
@@ -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
View File
@@ -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;
}
+23 -10
View File
@@ -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)) {