Compare commits
241
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fa9714793 | ||
|
|
d884793cb0 | ||
|
|
937d772c08 | ||
|
|
a82515673d | ||
|
|
c7ab2f2817 | ||
|
|
b8ba2db7a5 | ||
|
|
d871b94c52 | ||
|
|
4a819f7295 | ||
|
|
2ae368d2d6 | ||
|
|
9f35366449 | ||
|
|
dea0cd9e39 | ||
|
|
cc534b0ce4 | ||
|
|
84a4e6ac63 | ||
|
|
1b7e0c8a13 | ||
|
|
b0bf8753ed | ||
|
|
739ef662b9 | ||
|
|
d0aa29107d | ||
|
|
e25899b3ee | ||
|
|
01fa2bc2b6 | ||
|
|
ec6364b32b | ||
|
|
cd3bd1e1b1 | ||
|
|
332a385ccf | ||
|
|
5935144a5a | ||
|
|
1de403f39d | ||
|
|
f9b5383ae9 | ||
|
|
5dbbac35c3 | ||
|
|
414781db74 | ||
|
|
46e31c5cbd | ||
|
|
12a376c8fa | ||
|
|
46a5eb53f1 | ||
|
|
07a48bbc9c | ||
|
|
3aaa188d38 | ||
|
|
1286333ead | ||
|
|
6a77b56d34 | ||
|
|
a6cfcd4ccd | ||
|
|
80f81bd217 | ||
|
|
ccbcde70a6 | ||
|
|
f346fc68e0 | ||
|
|
7306ded7dd | ||
|
|
ae741aae61 | ||
|
|
b963b798df | ||
|
|
de2c6d0755 | ||
|
|
7f61df4052 | ||
|
|
367af9a1a7 | ||
|
|
23e1c6f9d1 | ||
|
|
345b015137 | ||
|
|
3c12bace28 | ||
|
|
817d3c10d1 | ||
|
|
900e7dee1c | ||
|
|
ca6015024b | ||
|
|
8733ce6148 | ||
|
|
85834a0498 | ||
|
|
732068ad9c | ||
|
|
ea434033f0 | ||
|
|
ebb71b4769 | ||
|
|
8c5668bf3d | ||
|
|
53baf0dfbf | ||
|
|
0c04caeb67 | ||
|
|
8b163c5734 | ||
|
|
a6007b5853 | ||
|
|
386b539f88 | ||
|
|
58c5912c30 | ||
|
|
17e07d525b | ||
|
|
dd019afff6 | ||
|
|
276a496ae8 | ||
|
|
9f10344fa7 | ||
|
|
c25d3f1b3c | ||
|
|
73e2144571 | ||
|
|
3d7f6b1820 | ||
|
|
d0079aa5f7 | ||
|
|
e2e6a40d9a | ||
|
|
99eae4cada | ||
|
|
de10e37a0c | ||
|
|
4758aecec2 | ||
|
|
c4e1c291d8 | ||
|
|
42127b0916 | ||
|
|
d03f653848 | ||
|
|
b1fd16cf06 | ||
|
|
b2cb88e4ff | ||
|
|
ee9bcc85ce | ||
|
|
4f0094a821 | ||
|
|
c2913c1026 | ||
|
|
13420b0e01 | ||
|
|
5567b53581 | ||
|
|
ff0fffccad | ||
|
|
38886e39bd | ||
|
|
25ef8ef7b0 | ||
|
|
6750d4778f | ||
|
|
7fc75eaee6 | ||
|
|
f1b090ee85 | ||
|
|
e6431dbe66 | ||
|
|
23c0cf69d7 | ||
|
|
e4be458433 | ||
|
|
0c8cb05237 | ||
|
|
5d9c058e10 | ||
|
|
af235987ea | ||
|
|
bae293ad94 | ||
|
|
1ad35cdcfd | ||
|
|
c0c3c2600f | ||
|
|
5413bae3ea | ||
|
|
e51f3ab507 | ||
|
|
e1f482ef5c | ||
|
|
f14023bd92 | ||
|
|
d68654e94f | ||
|
|
724e3413e4 | ||
|
|
f77ce71bd6 | ||
|
|
dc52003c01 | ||
|
|
3a0cb81a97 | ||
|
|
313475afec | ||
|
|
6226f2a037 | ||
|
|
2a09401600 | ||
|
|
2d3c9ce4ab | ||
|
|
273fb05833 | ||
|
|
dfe9748ff9 | ||
|
|
1b66cd3add | ||
|
|
a6f9236abf | ||
|
|
36fb17ac32 | ||
|
|
0e71aca89b | ||
|
|
ba36485180 | ||
|
|
0edd9e7b3c | ||
|
|
1fcd6aa245 | ||
|
|
1d0e71c5db | ||
|
|
5825d37e83 | ||
|
|
5b83d10d92 | ||
|
|
95c42b342b | ||
|
|
78cc6390f7 | ||
|
|
9b49db98d4 | ||
|
|
e6bf7be2f9 | ||
|
|
212476c3f6 | ||
|
|
42684f3864 | ||
|
|
e2e46a601b | ||
|
|
a1ac5f34ae | ||
|
|
1bf5b51679 | ||
|
|
b68546f824 | ||
|
|
841e559a70 | ||
|
|
f0af2efe8a | ||
|
|
c9d386badd | ||
|
|
f4834e0e7a | ||
|
|
1921464e27 | ||
|
|
f720782736 | ||
|
|
43598bf65a | ||
|
|
2bfe91695e | ||
|
|
22880b512a | ||
|
|
debee672f8 | ||
|
|
3d60998145 | ||
|
|
481da014bc | ||
|
|
d22395b96c | ||
|
|
aaa9fa67e9 | ||
|
|
ca2facdbc6 | ||
|
|
3d638ab2a1 | ||
|
|
30c11c5ca7 | ||
|
|
2a1144f0aa | ||
|
|
21e0f48fd5 | ||
|
|
9fc85141e0 | ||
|
|
184f50d034 | ||
|
|
6fae2de15f | ||
|
|
e188f932f4 | ||
|
|
71f329170f | ||
|
|
3ba33f85a0 | ||
|
|
1b553a9095 | ||
|
|
e9d157c87e | ||
|
|
47c09a3e4c | ||
|
|
759e337976 | ||
|
|
b15eda6ff1 | ||
|
|
71b27a387d | ||
|
|
2c734bd415 | ||
|
|
20f130f614 | ||
|
|
4eef9d9785 | ||
|
|
361635332c | ||
|
|
a42d7a0718 | ||
|
|
4b112f3a57 | ||
|
|
0de04a8dfa | ||
|
|
bace7b4a05 | ||
|
|
18e89595e0 | ||
|
|
d1129175bc | ||
|
|
c1cb272702 | ||
|
|
b7ff51adc5 | ||
|
|
fdc0f02b91 | ||
|
|
c4b93ff217 | ||
|
|
ca42ded4e2 | ||
|
|
30e44755fe | ||
|
|
2862d7a6e9 | ||
|
|
28784ed6ac | ||
|
|
6a12a43114 | ||
|
|
94d336f090 | ||
|
|
af0206d9cd | ||
|
|
65d2b1405f | ||
|
|
57d8da9577 | ||
|
|
2a754973cd | ||
|
|
e503759197 | ||
|
|
47baeaa5f7 | ||
|
|
0fba652770 | ||
|
|
9064d4eb42 | ||
|
|
f66c22027e | ||
|
|
614a934dc1 | ||
|
|
906c76a959 | ||
|
|
f2467f4411 | ||
|
|
aa68b41971 | ||
|
|
fdabe38180 | ||
|
|
ecdd973c3f | ||
|
|
fe641e9e7c | ||
|
|
0c3a87868c | ||
|
|
278e415e08 | ||
|
|
16882207bb | ||
|
|
a672fc76e4 | ||
|
|
1eef7bd70b | ||
|
|
49fd392f79 | ||
|
|
7178c4af53 | ||
|
|
551f90a617 | ||
|
|
b22e3ce391 | ||
|
|
662579cc6e | ||
|
|
bbec87abfa | ||
|
|
c147624d71 | ||
|
|
889530d8e6 | ||
|
|
5f3c71a96c | ||
|
|
985ada2f39 | ||
|
|
18d73dd4d5 | ||
|
|
f9ffa5a6b6 | ||
|
|
d259c35000 | ||
|
|
667278563d | ||
|
|
de61a3e515 | ||
|
|
d0240ddfab | ||
|
|
bb2ddb388e | ||
|
|
9bde337506 | ||
|
|
3267a121de | ||
|
|
df5a533983 | ||
|
|
2607f448ad | ||
|
|
8492db2e39 | ||
|
|
7c2d1cedb6 | ||
|
|
2d49840aba | ||
|
|
8d5cb3b289 | ||
|
|
5f6806ab33 | ||
|
|
9e877ac464 | ||
|
|
e960cca6ea | ||
|
|
4d4f354eef | ||
|
|
0e5d3fda91 | ||
|
|
d69faae5f4 | ||
|
|
48f8cc6890 | ||
|
|
4e477d9034 | ||
|
|
91ac3a6828 | ||
|
|
185dfcdb38 |
No files matched your search
@@ -53,3 +53,10 @@ Diese Datei enthaelt operative Arbeitsregeln fuer Agenten in diesem Repository.
|
||||
- Wenn Prozesse umgesetzt werden: verwende immer den Skill prozess-umsetzung
|
||||
- Wenn UI umgesetzt werden soll: verwende immer den Skill styleguide-anwendung
|
||||
- Zwingende Vorgabe für alle Arbeiten ist /docs/technical_architecture.md
|
||||
|
||||
## 9. MCP-Dokumentation
|
||||
- Bei jeder Änderung an der MCP-Schnittstelle des Marktmanagements ist neben der
|
||||
Repository-Dokumentation auch die MCP-Dokumentation unter
|
||||
`/Volumes/Hermes/Hermes Naurua/Marktmanagement/MCP` zu aktualisieren.
|
||||
- Die MCP-Dokumentation ist damit ein verbindlicher zusätzlicher Dokumentationsstand
|
||||
für alle zukünftigen MCP-Änderungen in diesem Projekt.
|
||||
@@ -20,6 +20,8 @@
|
||||
16. `db/migrations/0020_include_stock_adjustments_in_balance.sql`
|
||||
17. `db/migrations/0021_stock_lot_activation_selection.sql`
|
||||
18. `db/migrations/0022_backfill_stock_lot_activation_selection.sql`
|
||||
19. `db/migrations/0023_market_management.sql` bis `db/migrations/0034_market_occurrence_checklist.sql`
|
||||
20. `db/migrations/0035_market_occurrence_checklist_chapters.sql`
|
||||
|
||||
## Beispielausfuehrung
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
BEGIN;
|
||||
|
||||
-- Market Management base schema.
|
||||
-- Fachliche Ownership: market_management.
|
||||
|
||||
CREATE TABLE market_category (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_market_category_code ON market_category(code);
|
||||
CREATE UNIQUE INDEX uq_market_category_name ON market_category(name);
|
||||
|
||||
INSERT INTO market_category (code, name)
|
||||
VALUES
|
||||
('herbstmarkt', 'Herbstmärkte'),
|
||||
('weihnachtsmarkt', 'Weihnachtsmärkte'),
|
||||
('fruehlingsmarkt', 'Frühlingsmärkte'),
|
||||
('ostermarkt', 'Ostermärkte'),
|
||||
('kunsthandwerksmarkt', 'Kunsthandwerksmärkte'),
|
||||
('regionalmarkt', 'Regionalmärkte'),
|
||||
('weitere_geeignete_maerkte', 'Weitere geeignete Märkte');
|
||||
|
||||
CREATE TABLE market_municipality_reference (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
bfs_municipality_number INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
canton_code TEXT NOT NULL,
|
||||
data_version DATE NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_market_municipality_reference_bfs_number_version
|
||||
ON market_municipality_reference(bfs_municipality_number, data_version);
|
||||
CREATE INDEX idx_market_municipality_reference_canton
|
||||
ON market_municipality_reference(canton_code, name);
|
||||
|
||||
CREATE TABLE market_definition (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
category_id BIGINT NOT NULL REFERENCES market_category(id) ON DELETE RESTRICT,
|
||||
archived_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_market_definition_name ON market_definition(name);
|
||||
CREATE INDEX idx_market_definition_category ON market_definition(category_id);
|
||||
|
||||
CREATE TABLE market_occurrence (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
market_definition_id BIGINT NOT NULL REFERENCES market_definition(id) ON DELETE RESTRICT,
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
place TEXT,
|
||||
address TEXT,
|
||||
municipality_reference_id BIGINT REFERENCES market_municipality_reference(id) ON DELETE RESTRICT,
|
||||
notes TEXT,
|
||||
registration_deadline DATE,
|
||||
request_or_registration_date DATE,
|
||||
confirmation_date DATE,
|
||||
payment_deadline DATE,
|
||||
stand_size TEXT,
|
||||
table_available BOOLEAN,
|
||||
table_rentable BOOLEAN,
|
||||
own_table_required BOOLEAN,
|
||||
shelter_available BOOLEAN,
|
||||
power_available BOOLEAN,
|
||||
water_available BOOLEAN,
|
||||
lighting_available BOOLEAN,
|
||||
other_infrastructure TEXT,
|
||||
own_stand_requirements TEXT,
|
||||
travel_address TEXT,
|
||||
travel_distance_km NUMERIC(10, 2),
|
||||
travel_duration_minutes INTEGER,
|
||||
parking_options TEXT,
|
||||
delivery_access TEXT,
|
||||
direct_unload_possible BOOLEAN,
|
||||
storage_options TEXT,
|
||||
travel_notes TEXT,
|
||||
payment_status TEXT NOT NULL DEFAULT 'unpaid',
|
||||
payment_date DATE,
|
||||
paid_amount NUMERIC(14, 2),
|
||||
payment_method_code TEXT,
|
||||
cancellation_terms TEXT,
|
||||
revenue_share_terms TEXT,
|
||||
other_participation_conditions TEXT,
|
||||
contract_folder_url TEXT,
|
||||
archived_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_market_occurrence_status CHECK (
|
||||
status IN ('open', 'waitlist', 'registered', 'confirmation_pending', 'confirmed', 'cancelled', 'completed')
|
||||
),
|
||||
CONSTRAINT chk_market_occurrence_payment_status CHECK (
|
||||
payment_status IN ('paid', 'unpaid')
|
||||
),
|
||||
CONSTRAINT chk_market_occurrence_travel_distance CHECK (
|
||||
travel_distance_km IS NULL OR travel_distance_km >= 0
|
||||
),
|
||||
CONSTRAINT chk_market_occurrence_travel_duration CHECK (
|
||||
travel_duration_minutes IS NULL OR travel_duration_minutes >= 0
|
||||
),
|
||||
CONSTRAINT chk_market_occurrence_paid_amount CHECK (
|
||||
paid_amount IS NULL OR paid_amount >= 0
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_market_occurrence_market ON market_occurrence(market_definition_id);
|
||||
CREATE INDEX idx_market_occurrence_status ON market_occurrence(status);
|
||||
CREATE INDEX idx_market_occurrence_municipality
|
||||
ON market_occurrence(municipality_reference_id);
|
||||
CREATE INDEX idx_market_occurrence_archived_at ON market_occurrence(archived_at);
|
||||
|
||||
CREATE TABLE market_occurrence_organizer (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
occurrence_id BIGINT NOT NULL UNIQUE REFERENCES market_occurrence(id) ON DELETE RESTRICT,
|
||||
organization_contact_id BIGINT NOT NULL,
|
||||
contact_person_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_market_occurrence_organizer_organization
|
||||
ON market_occurrence_organizer(organization_contact_id);
|
||||
CREATE INDEX idx_market_occurrence_organizer_contact_person
|
||||
ON market_occurrence_organizer(contact_person_id);
|
||||
|
||||
CREATE TABLE market_occurrence_day (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
occurrence_id BIGINT NOT NULL REFERENCES market_occurrence(id) ON DELETE RESTRICT,
|
||||
date DATE NOT NULL,
|
||||
stand_start_time TIME,
|
||||
stand_end_time TIME,
|
||||
setup_start_time TIME,
|
||||
setup_end_time TIME,
|
||||
delivery_start_time TIME,
|
||||
delivery_end_time TIME,
|
||||
observations TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_market_occurrence_day_stand_time CHECK (
|
||||
stand_start_time IS NULL OR stand_end_time IS NULL OR stand_end_time >= stand_start_time
|
||||
),
|
||||
CONSTRAINT chk_market_occurrence_day_setup_time CHECK (
|
||||
setup_start_time IS NULL OR setup_end_time IS NULL OR setup_end_time >= setup_start_time
|
||||
),
|
||||
CONSTRAINT chk_market_occurrence_day_delivery_time CHECK (
|
||||
delivery_start_time IS NULL OR delivery_end_time IS NULL OR delivery_end_time >= delivery_start_time
|
||||
)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_market_occurrence_day_date ON market_occurrence_day(occurrence_id, date);
|
||||
CREATE INDEX idx_market_occurrence_day_date ON market_occurrence_day(date);
|
||||
|
||||
CREATE TABLE market_occurrence_cost_item (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
occurrence_id BIGINT NOT NULL REFERENCES market_occurrence(id) ON DELETE RESTRICT,
|
||||
cost_type TEXT NOT NULL,
|
||||
description TEXT,
|
||||
gross_amount NUMERIC(14, 2) NOT NULL,
|
||||
tax_status TEXT NOT NULL,
|
||||
tax_rate NUMERIC(5, 2) NOT NULL DEFAULT 0,
|
||||
tax_amount NUMERIC(14, 2) GENERATED ALWAYS AS (
|
||||
CASE
|
||||
WHEN tax_status = 'tax_free' THEN 0
|
||||
ELSE ROUND(gross_amount - gross_amount / (1 + tax_rate / 100), 2)
|
||||
END
|
||||
) STORED,
|
||||
net_amount NUMERIC(14, 2) GENERATED ALWAYS AS (
|
||||
gross_amount - CASE
|
||||
WHEN tax_status = 'tax_free' THEN 0
|
||||
ELSE ROUND(gross_amount - gross_amount / (1 + tax_rate / 100), 2)
|
||||
END
|
||||
) STORED,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_market_occurrence_cost_gross_nonnegative CHECK (gross_amount >= 0),
|
||||
CONSTRAINT chk_market_occurrence_cost_tax_status CHECK (tax_status IN ('tax_free', 'taxable')),
|
||||
CONSTRAINT chk_market_occurrence_cost_tax_rate CHECK (
|
||||
(tax_status = 'tax_free' AND tax_rate = 0)
|
||||
OR (tax_status = 'taxable' AND tax_rate > 0)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_market_occurrence_cost_item_occurrence
|
||||
ON market_occurrence_cost_item(occurrence_id);
|
||||
|
||||
CREATE TABLE market_mcp_idempotency (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_subject TEXT NOT NULL,
|
||||
client_id TEXT NOT NULL,
|
||||
idempotency_key TEXT NOT NULL,
|
||||
tool_name TEXT NOT NULL,
|
||||
request_hash TEXT NOT NULL,
|
||||
result_status TEXT NOT NULL,
|
||||
result_entity_type TEXT,
|
||||
result_entity_id TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
CONSTRAINT chk_market_mcp_idempotency_result_status CHECK (
|
||||
result_status IN ('processing', 'succeeded', 'failed')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_market_mcp_idempotency_client_key
|
||||
ON market_mcp_idempotency(client_id, idempotency_key);
|
||||
CREATE INDEX idx_market_mcp_idempotency_expiry ON market_mcp_idempotency(expires_at);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE market_mcp_idempotency
|
||||
ADD COLUMN result_payload JSONB;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,10 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE market_definition
|
||||
ADD COLUMN website_url TEXT;
|
||||
|
||||
ALTER TABLE market_definition
|
||||
ADD CONSTRAINT chk_market_definition_website_url
|
||||
CHECK (website_url IS NULL OR website_url ~* '^https?://');
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE market_occurrence_day
|
||||
ADD COLUMN inherit_from_first_day BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,28 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE sales_order
|
||||
ADD COLUMN market_occurrence_id BIGINT,
|
||||
ADD COLUMN market_occurrence_day_id BIGINT;
|
||||
|
||||
ALTER TABLE market_occurrence_day
|
||||
ADD CONSTRAINT uq_market_occurrence_day_occurrence_id UNIQUE (occurrence_id, id);
|
||||
|
||||
ALTER TABLE sales_order
|
||||
ADD CONSTRAINT fk_sales_order_market_occurrence
|
||||
FOREIGN KEY (market_occurrence_id)
|
||||
REFERENCES market_occurrence (id)
|
||||
ON DELETE RESTRICT,
|
||||
ADD CONSTRAINT fk_sales_order_market_occurrence_day
|
||||
FOREIGN KEY (market_occurrence_id, market_occurrence_day_id)
|
||||
REFERENCES market_occurrence_day (occurrence_id, id)
|
||||
ON DELETE RESTRICT,
|
||||
ADD CONSTRAINT chk_sales_order_market_link_complete
|
||||
CHECK ((market_occurrence_id IS NULL) = (market_occurrence_day_id IS NULL));
|
||||
|
||||
CREATE INDEX idx_sales_order_market_occurrence
|
||||
ON sales_order (market_occurrence_id);
|
||||
|
||||
CREATE INDEX idx_sales_order_market_occurrence_day
|
||||
ON sales_order (market_occurrence_day_id);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,55 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE market_organizer (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT,
|
||||
phone TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_market_organizer_status CHECK (status IN ('active', 'inactive'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_market_organizer_name ON market_organizer (LOWER(TRIM(name)));
|
||||
|
||||
INSERT INTO market_organizer (name)
|
||||
VALUES ('Beispielveranstalter 1');
|
||||
|
||||
ALTER TABLE market_definition
|
||||
ADD COLUMN organizer_id BIGINT;
|
||||
|
||||
UPDATE market_definition
|
||||
SET organizer_id = (SELECT id FROM market_organizer WHERE name = 'Beispielveranstalter 1');
|
||||
|
||||
ALTER TABLE market_definition
|
||||
ALTER COLUMN organizer_id SET NOT NULL,
|
||||
ADD CONSTRAINT fk_market_definition_organizer
|
||||
FOREIGN KEY (organizer_id) REFERENCES market_organizer(id) ON DELETE RESTRICT;
|
||||
|
||||
CREATE INDEX idx_market_definition_organizer ON market_definition (organizer_id);
|
||||
|
||||
ALTER TABLE market_occurrence_organizer
|
||||
ADD COLUMN contact_person_name TEXT,
|
||||
ADD COLUMN contact_email TEXT,
|
||||
ADD COLUMN contact_phone TEXT;
|
||||
|
||||
UPDATE market_occurrence_organizer oo
|
||||
SET contact_person_name = NULLIF(TRIM(CONCAT_WS(' ', c.first_name, c.last_name)), ''),
|
||||
contact_email = c.email,
|
||||
contact_phone = c.phone
|
||||
FROM contact c
|
||||
WHERE c.id = oo.contact_person_id;
|
||||
|
||||
ALTER TABLE market_occurrence_organizer
|
||||
DROP COLUMN organization_contact_id,
|
||||
DROP COLUMN contact_person_id;
|
||||
|
||||
INSERT INTO market_occurrence_organizer (occurrence_id, contact_person_name, created_at, updated_at)
|
||||
SELECT o.id, NULL, NOW(), NOW()
|
||||
FROM market_occurrence o
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM market_occurrence_organizer oo WHERE oo.occurrence_id = o.id
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,40 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE market_municipality (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
municipality_reference_id BIGINT UNIQUE REFERENCES market_municipality_reference(id) ON DELETE RESTRICT,
|
||||
name TEXT NOT NULL,
|
||||
canton_code TEXT,
|
||||
bfs_municipality_number INTEGER,
|
||||
data_version DATE,
|
||||
source TEXT NOT NULL DEFAULT 'local',
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_market_municipality_source CHECK (source IN ('reference', 'local'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_market_municipality_name
|
||||
ON market_municipality (LOWER(TRIM(name)));
|
||||
CREATE INDEX idx_market_municipality_name
|
||||
ON market_municipality (name);
|
||||
|
||||
INSERT INTO market_municipality (
|
||||
municipality_reference_id, name, canton_code, bfs_municipality_number, data_version, source, is_active
|
||||
)
|
||||
SELECT id, name, canton_code, bfs_municipality_number, data_version, 'reference', is_active
|
||||
FROM market_municipality_reference;
|
||||
|
||||
ALTER TABLE market_occurrence
|
||||
DROP CONSTRAINT market_occurrence_municipality_reference_id_fkey;
|
||||
|
||||
UPDATE market_occurrence o
|
||||
SET municipality_reference_id = m.id
|
||||
FROM market_municipality m
|
||||
WHERE m.municipality_reference_id = o.municipality_reference_id;
|
||||
|
||||
ALTER TABLE market_occurrence
|
||||
ADD CONSTRAINT fk_market_occurrence_municipality
|
||||
FOREIGN KEY (municipality_reference_id) REFERENCES market_municipality(id) ON DELETE RESTRICT;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,63 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE market_cost_type (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
is_system BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
INSERT INTO market_cost_type (code, name, is_system)
|
||||
VALUES ('stand_fee', 'Standkosten', TRUE);
|
||||
|
||||
ALTER TABLE market_occurrence_cost_item
|
||||
ADD COLUMN cost_type_id BIGINT REFERENCES market_cost_type(id) ON DELETE RESTRICT;
|
||||
|
||||
INSERT INTO market_cost_type (code, name)
|
||||
SELECT DISTINCT
|
||||
'legacy_' || md5(trim(cost_type)),
|
||||
trim(cost_type)
|
||||
FROM market_occurrence_cost_item
|
||||
WHERE trim(cost_type) <> ''
|
||||
AND lower(trim(cost_type)) <> 'standkosten'
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
UPDATE market_occurrence_cost_item item
|
||||
SET cost_type_id = type.id
|
||||
FROM market_cost_type type
|
||||
WHERE type.name = CASE
|
||||
WHEN lower(trim(item.cost_type)) = 'standkosten' THEN 'Standkosten'
|
||||
ELSE trim(item.cost_type)
|
||||
END;
|
||||
|
||||
UPDATE market_occurrence_cost_item item
|
||||
SET cost_type_id = type.id
|
||||
FROM market_cost_type type
|
||||
WHERE type.code = 'stand_fee'
|
||||
AND item.id = (
|
||||
SELECT MIN(first_item.id)
|
||||
FROM market_occurrence_cost_item first_item
|
||||
WHERE first_item.occurrence_id = item.occurrence_id
|
||||
);
|
||||
|
||||
INSERT INTO market_occurrence_cost_item (occurrence_id, cost_type_id, description, gross_amount, tax_status, tax_rate)
|
||||
SELECT occurrence.id, type.id, NULL, 0.00, 'taxable', 8.10
|
||||
FROM market_occurrence occurrence
|
||||
JOIN market_cost_type type ON type.code = 'stand_fee'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM market_occurrence_cost_item item WHERE item.occurrence_id = occurrence.id
|
||||
);
|
||||
|
||||
ALTER TABLE market_occurrence_cost_item
|
||||
ALTER COLUMN cost_type_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE market_occurrence_cost_item
|
||||
DROP COLUMN cost_type;
|
||||
|
||||
CREATE INDEX idx_market_occurrence_cost_item_type
|
||||
ON market_occurrence_cost_item(cost_type_id);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,11 @@
|
||||
BEGIN;
|
||||
|
||||
UPDATE market_occurrence_cost_item item
|
||||
SET tax_status = 'taxable', tax_rate = 8.10, updated_at = NOW()
|
||||
WHERE item.id = (
|
||||
SELECT MIN(first_item.id)
|
||||
FROM market_occurrence_cost_item first_item
|
||||
WHERE first_item.occurrence_id = item.occurrence_id
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,8 @@
|
||||
BEGIN;
|
||||
|
||||
-- Reisedauer wird fachlich nicht mehr verwendet und vollständig entfernt.
|
||||
ALTER TABLE market_occurrence
|
||||
DROP CONSTRAINT IF EXISTS chk_market_occurrence_travel_duration,
|
||||
DROP COLUMN IF EXISTS travel_duration_minutes;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE market_occurrence
|
||||
ADD COLUMN registration_note TEXT;
|
||||
@@ -0,0 +1,33 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE market_occurrence_checklist (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
occurrence_id BIGINT NOT NULL UNIQUE REFERENCES market_occurrence(id) ON DELETE RESTRICT,
|
||||
comment TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE market_occurrence_checklist_item (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
occurrence_id BIGINT NOT NULL REFERENCES market_occurrence(id) ON DELETE RESTRICT,
|
||||
position INTEGER NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
is_completed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_market_occurrence_checklist_item_position CHECK (position > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_market_occurrence_checklist_item_occurrence
|
||||
ON market_occurrence_checklist_item(occurrence_id, position, id);
|
||||
|
||||
INSERT INTO market_occurrence_checklist (occurrence_id)
|
||||
SELECT id FROM market_occurrence;
|
||||
|
||||
INSERT INTO market_occurrence_checklist_item (occurrence_id, position, label)
|
||||
SELECT occurrence_id, position, 'Position ' || position
|
||||
FROM market_occurrence_checklist
|
||||
CROSS JOIN generate_series(1, 30) AS positions(position);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,53 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE market_occurrence_checklist_item
|
||||
ADD COLUMN chapter_key TEXT;
|
||||
|
||||
DELETE FROM market_occurrence_checklist_item;
|
||||
UPDATE market_occurrence_checklist
|
||||
SET comment = NULL,
|
||||
updated_at = NOW();
|
||||
|
||||
ALTER TABLE market_occurrence_checklist_item
|
||||
ALTER COLUMN chapter_key SET NOT NULL,
|
||||
ADD CONSTRAINT chk_market_occurrence_checklist_item_chapter
|
||||
CHECK (chapter_key IN ('products_sales', 'decoration', 'stand_equipment', 'technology'));
|
||||
|
||||
INSERT INTO market_occurrence_checklist_item
|
||||
(occurrence_id, chapter_key, position, label, is_completed)
|
||||
SELECT checklist.occurrence_id,
|
||||
defaults.chapter_key,
|
||||
defaults.position,
|
||||
defaults.label,
|
||||
FALSE
|
||||
FROM market_occurrence_checklist checklist
|
||||
CROSS JOIN (
|
||||
VALUES
|
||||
('products_sales', 1, 'Fläschchen'),
|
||||
('products_sales', 2, 'Säckchen'),
|
||||
('products_sales', 3, 'Dunkle Kleber'),
|
||||
('products_sales', 4, 'Naurua-Kleber'),
|
||||
('products_sales', 5, 'Anwendungsbeilage'),
|
||||
('products_sales', 6, 'Flyer Naurua'),
|
||||
('products_sales', 7, 'Flyer Vitalpilze'),
|
||||
('products_sales', 8, 'Vitalpilzbuch'),
|
||||
('products_sales', 9, 'SumUp-Terminal'),
|
||||
('products_sales', 10, 'Bargeld'),
|
||||
('decoration', 1, 'Dekoration'),
|
||||
('decoration', 2, 'Pilze'),
|
||||
('decoration', 3, 'Blumenvase'),
|
||||
('decoration', 4, 'Blume'),
|
||||
('stand_equipment', 1, 'Tisch'),
|
||||
('stand_equipment', 2, 'Tischtücher'),
|
||||
('stand_equipment', 3, 'Beleuchtung'),
|
||||
('stand_equipment', 4, 'Holzklötze'),
|
||||
('stand_equipment', 5, 'Wagen'),
|
||||
('stand_equipment', 6, 'Sonnenschirm'),
|
||||
('stand_equipment', 7, 'Parkkarte/-Marktinformationen'),
|
||||
('technology', 1, 'Batterie-/Powerbank fürs Handy')
|
||||
) AS defaults(chapter_key, position, label);
|
||||
|
||||
CREATE INDEX idx_market_occurrence_checklist_item_chapter
|
||||
ON market_occurrence_checklist_item(occurrence_id, chapter_key, position, id);
|
||||
|
||||
COMMIT;
|
||||
@@ -42,6 +42,9 @@ Bestand, Charge, MHD und Bewegungen.
|
||||
## Technisch umgesetzt
|
||||
|
||||
- `modules/erp/lager/service.php`
|
||||
- Read-only MCP für Hermes: `modules/erp/lager/mcp.php`
|
||||
- MCP-Endpoint: `public/api/erp-lager-mcp.php`
|
||||
- [ERP-Lager-Hermes-MCP-API](konzepte/erp_lager_hermes_mcp_api.md)
|
||||
|
||||
Hersteller-Stammdaten gehören zum Modul `kontakte`. Eine reale Lagercharge kann dort einen Hersteller und dessen Hersteller-Chargennummer referenzieren; die interne Naurua-Chargennummer bleibt Eigentum des Moduls `lager`. Virtuelle Fehlbestandschargen haben keinen Herstellerbezug.
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Hermes: ERP-Lager-MCP-API
|
||||
|
||||
## Zweck
|
||||
|
||||
Das ERP-Lager stellt ein eigenes, read-only MCP-Interface bereit. Hermes kann dadurch ausschließlich die Lager-Tools laden, ohne Marktmanagement-Tools in seinen Kontext aufzunehmen.
|
||||
|
||||
Endpoint: `https://erpnaurua.imhochrain.ch/api/erp-lager-mcp.php`
|
||||
|
||||
Authentifizierung: `Authorization: Bearer <MARKET_MCP_TOKEN>`
|
||||
|
||||
Der Token ist identisch mit dem Marktmanagement-MCP, weil derselbe Hermes-Bot auf beide fachlich getrennten Interfaces zugreift. Die Toolsets und Endpoints bleiben trotzdem getrennt.
|
||||
|
||||
## Verfügbare Tools
|
||||
|
||||
### `erp_lager.list_products`
|
||||
|
||||
Liefert alle aktiven Produkte mit ihren stabilen IDs.
|
||||
|
||||
### `erp_lager.get_product`
|
||||
|
||||
Liest alle Lagerdaten eines aktiven Produkts. Eingabe: `{ "product_id": 12 }`.
|
||||
|
||||
Die Antwort enthält die GUI-parallele Produktübersicht mit Produktstammdaten, aktueller und verfügbarer Lagermenge, Menge im Wareneingang, aktueller Charge, Ausverkaufsdatum und Forecast-Status, nächster aktiver offener Charge, weiteren offenen zukünftigen Chargen, historischen geschlossenen Chargen sowie virtueller Fehlbestandsmenge und virtueller Fehlbestandscharge.
|
||||
|
||||
Das Interface ist ausschließlich lesend. Es gibt keine Lager-MCP-Tools zum Anlegen, Ändern, Buchen, Verschieben oder Löschen.
|
||||
|
||||
## JSON-RPC
|
||||
|
||||
Das Interface unterstützt `initialize`, `notifications/initialized`, `tools/list` und `tools/call` gemäß dem bestehenden Marktmanagement-MCP-Transport.
|
||||
|
||||
Bei `tools/list` ist `inputSchema.properties` für jedes Werkzeug immer ein JSON-Objekt; beim parameterlosen `erp_lager.list_products` ist es `{}`.
|
||||
@@ -22,7 +22,7 @@ Direkt ausgeführter Lager-Prozess mit zwei Triggern: synchron nach jeder erfolg
|
||||
3. Verkaufsallokationen der letzten 365 Tage aus `sales_order_line_lot_allocation`, `sales_order_line` und `sales_order` laden.
|
||||
4. `allocated`- und `reserved`-Mengen nach Bestell- beziehungsweise OTC-Verkaufsdatum je Produkt und Kalendertag aggregieren.
|
||||
5. Stornierte Auftrags- und Positionsmengen aus der Nachfrage herausrechnen.
|
||||
6. Den gewichteten Tagesabsatz mit einer Halbwertszeit von 60 Tagen berechnen.
|
||||
6. Den gewichteten Tagesabsatz ab dem ersten gültigen Verkaufstag innerhalb des geladenen Zeitraums mit einer Halbwertszeit von 60 Tagen berechnen; Tage vor dem ersten Verkauf werden nicht als Nullabsatz berücksichtigt.
|
||||
7. Das Leer-Datum der aktuellen Charge aus ihrem physischen Bestand und der gewichteten Nachfrage berechnen. Das Leer-Datum der nächsten Charge wird mit derselben Nachfrage ab dem Leer-Datum der aktuellen Charge weitergerechnet; bei bereits leerer aktueller Charge beginnt es am aktuellen Datum.
|
||||
8. Jede erfolgreiche Berechnung je Charge in `stock_lot_forecast_history` speichern und den jeweiligen Snapshot in `stock_lot` aktualisieren.
|
||||
9. Im Cron Fehler einzelner Produkte sammeln und mit den übrigen Produkten fortfahren.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Market Management
|
||||
|
||||
Stand: 2026-09-15
|
||||
Status: Fachliches Zielbild / Anforderungen in Ausarbeitung
|
||||
|
||||
## Zweck
|
||||
|
||||
Das Modul verwaltet Märkte von der Recherche und Prüfung über Anfrage, Anmeldung
|
||||
und Durchführung bis zur Kosten-, Zahlungs- und Erlösauswertung.
|
||||
|
||||
## Fachliche Ownership
|
||||
|
||||
Das Modul soll die fachliche Ownership für Marktstammdaten, Markttermine,
|
||||
Teilnahme- und Durchführungsstatus, marktbezogene Kosten und Zahlungen sowie
|
||||
Erlös- und Tagesauswertungen tragen.
|
||||
|
||||
## Zugriffsmodell
|
||||
|
||||
Die Nutzung des Moduls ist an eine Benutzeridentität gebunden. Das Modul soll
|
||||
separat von der übrigen Anwendung nutzbar und an weitere Personen verkaufbar
|
||||
sein.
|
||||
|
||||
Die Ownership der Lizenz-/Entitlement-Daten ist noch nicht entschieden und
|
||||
bleibt offen.
|
||||
|
||||
## MCP-Schnittstelle zu Hermes
|
||||
|
||||
Die App stellt die MCP-Server-Schnittstelle bereit. Hermes Agent ist MCP-Client
|
||||
und ruft ausschließlich freigegebene Funktionen des Marktmanagement-Moduls
|
||||
auf.
|
||||
|
||||
Die App behält die Kontrolle über:
|
||||
|
||||
- Authentifizierung und Berechtigungsprüfung
|
||||
- Validierung und fachliche Interpretation der Eingaben
|
||||
- erlaubte Schreiboperationen
|
||||
- Persistierung in der eigenen Datenbank
|
||||
- Audit- und Quelleninformationen
|
||||
|
||||
Hermes erhält keinen direkten Zugriff auf die Datenbank. Das Zielbild und die
|
||||
Schnittstellenentscheidungen sind im
|
||||
[market_management_hermes_mcp_konzept.md](konzepte/market_management_hermes_mcp_konzept.md)
|
||||
dokumentiert.
|
||||
|
||||
## Abgrenzung
|
||||
|
||||
- Zunächst keine eigene Benutzerverwaltung.
|
||||
- Keine eigene technische Runtime- oder Scheduler-Logik.
|
||||
- Fachlich neutrale UI- und technische Querschnittsbausteine gehören nach
|
||||
`shared`.
|
||||
|
||||
## Dokumente
|
||||
|
||||
- [Anforderungsprofil](requirements/market_management_requirements.md)
|
||||
- [Datenarchitektur](data/market_management_data_architecture.md)
|
||||
- [Technisches Tabellen- und Prozesskonzept](konzepte/market_management_technisches_konzept.md)
|
||||
- [Hermes-MCP-Konzept](konzepte/market_management_hermes_mcp_konzept.md)
|
||||
- [DB-Ownership-Konzept](konzepte/market_management_db_ownership.md)
|
||||
- Prozessdokumente werden unter `processes/main/` und
|
||||
`processes/sub_processes/` angelegt, sobald die fachlichen Prozesse explizit
|
||||
definiert sind.
|
||||
|
||||
## Offene Architekturentscheidung
|
||||
|
||||
Die Ownership und konkrete Ablage der Lizenz-/Entitlement-Daten ist offen.
|
||||
Diese Entscheidung darf bei der weiteren Umsetzung nicht stillschweigend
|
||||
vorweggenommen werden.
|
||||
@@ -0,0 +1,106 @@
|
||||
# market_management_data_architecture
|
||||
|
||||
Stand: 2026-09-18
|
||||
Status: Bestätigtes Datenarchitektur-Zielbild
|
||||
|
||||
## 1. Ziel
|
||||
|
||||
Das Marktmanagement-Modul muss wiederkehrende Märkte mit mehreren konkreten
|
||||
Durchführungen und mehreren Tagen je Durchführung ohne redundante Vermischung
|
||||
der Daten abbilden können.
|
||||
|
||||
## 2. Bestätigte Hierarchie
|
||||
|
||||
```text
|
||||
Kategorie 1 ─── n Marktdefinition 1 ─── n Durchführung 1 ─── n Durchführungstag
|
||||
```
|
||||
|
||||
Eine Kategorie enthält beliebig viele Marktdefinitionen. Jede Marktdefinition
|
||||
gehört genau einer Kategorie.
|
||||
|
||||
### Kategorie
|
||||
|
||||
Eine Kategorie ordnet Märkte fachlich. Fabienne darf die deterministische
|
||||
Kategorie-/Marktartliste erweitern. Hermes darf bestehende Kategorien
|
||||
verwenden, aber keine Kategorien anlegen.
|
||||
|
||||
### Marktdefinition
|
||||
|
||||
Eine Marktdefinition ist die übergeordnete fachliche Identität, unter der
|
||||
mehrere konkrete Durchführungen geführt werden können. Der zentrale Marktstamm
|
||||
enthält nur den eindeutigen Marktnamen und die Marktart/Kategorie.
|
||||
|
||||
### Durchführung
|
||||
|
||||
Eine Durchführung ist eine konkrete Ausgabe bzw. Teilnahme an einem Markt. Die
|
||||
Durchführung kann einen oder mehrere Durchführungstage umfassen.
|
||||
|
||||
### Durchführungstag
|
||||
|
||||
Ein Durchführungstag ist ein einzelner Tag innerhalb einer konkreten
|
||||
Durchführung. Tagesbezogene Zeit-, Erlös- und Verkaufsdaten gehören auf diese
|
||||
Ebene.
|
||||
|
||||
## 3. Feld- und Referenzwert-Zuordnung
|
||||
|
||||
### Zentraler Marktstamm
|
||||
|
||||
- Marktname
|
||||
- Marktart/Kategorie aus einer deterministischen Liste
|
||||
- optionale Website-URL der Veranstaltung
|
||||
|
||||
### Durchführung
|
||||
|
||||
Alle übrigen veränderlichen Marktangaben werden pro Durchführung erfasst,
|
||||
insbesondere Kontakt, Ort, Termine, Anmeldung, Status, Stand, Infrastruktur,
|
||||
Logistik, Kosten, Zahlung, Vertragslink und Notizen.
|
||||
|
||||
### Durchführungstag
|
||||
|
||||
- Datum des Durchführungstags
|
||||
- Standzeit und tagesbezogene Durchführungszeiten
|
||||
- Bruttoerlös aus zugeordneten OTC-Verkäufen
|
||||
- Anzahl verkaufter Artikel aus zugeordneten OTC-Positionen
|
||||
- Ereignisse und Beobachtungen
|
||||
|
||||
### Deterministische Referenzwerte
|
||||
|
||||
- Marktart/Kategorie
|
||||
- Marktstatus
|
||||
- Zahlungsstatus
|
||||
- Zahlungsart
|
||||
- Kanton
|
||||
- standardisierte Ja/Nein-Merkmale für Infrastrukturangaben
|
||||
|
||||
### Abgeleitete Werte
|
||||
|
||||
- Wochentag aus dem Datum
|
||||
- Saison aus dem Durchführungsdatum
|
||||
- Gesamtkosten aus dem erfassten Bruttobetrag
|
||||
- offener Betrag aus Kosten und bezahltem Betrag
|
||||
- Gesamterlös, Durchschnittswerte und Gesamtzahl verkaufter Artikel aus den
|
||||
Durchführungstagen
|
||||
|
||||
## 4. Ownership- und Normalisierungsregeln
|
||||
|
||||
- Jede Ebene erhält eine eigene stabile Identität.
|
||||
- Mehrere Durchführungstage werden nicht als wiederholte Spalten in der
|
||||
Durchführung gespeichert.
|
||||
- Mehrere Durchführungen werden nicht als wiederholte Spalten im Markt
|
||||
gespeichert.
|
||||
- Tageserlöse und Tagesverkaufszahlen werden nur für den Durchführungstag aus
|
||||
zugeordneten OTC-Verkäufen abgeleitet.
|
||||
- Aggregierte Werte für Durchführung oder Markt werden aus den untergeordneten
|
||||
Durchführungstagen abgeleitet oder als klar definierte Auswertung bereitgestellt.
|
||||
- MCP-Schreiboperationen müssen eindeutig an die betroffene Ebene gebunden sein.
|
||||
- Fremde Modul- oder DB-Ownership wird durch diese Datenarchitektur nicht
|
||||
eingeführt.
|
||||
|
||||
## 5. Bestätigte Ergänzungen
|
||||
|
||||
- Eine Durchführung ist eine konkrete Teilnahme bzw. Veranstaltungsausgabe.
|
||||
- Jeder Durchführungstag ist eine eigene Subdurchführung mit eigenen OTC-
|
||||
Verkäufen und abgeleiteten Tageswerten.
|
||||
- Markt und Durchführung werden unabhängig voneinander archiviert und
|
||||
reaktiviert.
|
||||
- Der Zahlungsstatus ist manuell und besitzt nur `paid` oder `unpaid`.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Market Management: DB-Ownership-Konzept
|
||||
|
||||
Stand: 2026-09-19
|
||||
Status: Verbindliches DB-Ownership-Konzept
|
||||
|
||||
## 1. Owned Tabellen des Moduls
|
||||
|
||||
| Tabelle | Ownership | Schreibverantwortung |
|
||||
|---|---|---|
|
||||
| `market_category` | `market_management` | Marktmanagement-Service |
|
||||
| `market_definition` | `market_management` | Marktmanagement-Service |
|
||||
| `market_occurrence` | `market_management` | Marktmanagement-Service |
|
||||
| `market_occurrence_organizer` | `market_management` | Marktmanagement-Service über Kontakt-Schnittstelle |
|
||||
| `market_occurrence_day` | `market_management` | Marktmanagement-Service |
|
||||
| `market_occurrence_cost_item` | `market_management` | Marktmanagement-Service |
|
||||
| `market_occurrence_checklist` | `market_management` | Submodul `occurrence_checklist` |
|
||||
| `market_occurrence_checklist_item` | `market_management` | Submodul `occurrence_checklist` |
|
||||
| `market_municipality_reference` | `market_management` | Referenzdatenprozess |
|
||||
| `market_municipality` | `market_management` | Marktmanagement-Service |
|
||||
| `market_mcp_idempotency` | `market_management` | MCP-Submodul |
|
||||
|
||||
## 2. Fremde Datenreferenzen
|
||||
|
||||
### Kontaktmodul
|
||||
|
||||
Das Marktmanagement-Modul speichert nur die stabilen IDs von Organisation und
|
||||
Kontaktperson. Es schreibt keine Kontakt-, Organisations- oder Adressdaten in
|
||||
fremde Tabellen.
|
||||
|
||||
### OTC-/Direktverkaufsmodul
|
||||
|
||||
Das OTC-Modul bleibt Owner seiner Verkaufstabellen. Jeder neue OTC-Verkauf
|
||||
referenziert einen gültigen `market_occurrence_day_id`. Marktmanagement liest
|
||||
die Verkaufsdaten ausschließlich über die definierte Schnittstelle für die
|
||||
Umsatz- und Artikelmengenaggregation.
|
||||
|
||||
## 3. Schreibgrenzen
|
||||
|
||||
- Direkte Cross-Modul-Writes sind verboten.
|
||||
- Marktmanagement schreibt nicht in OTC-, Kontakt- oder Organisations-Tabellen.
|
||||
- Das OTC-Modul schreibt nicht in Marktmanagement-Tabellen.
|
||||
- MCP schreibt nie direkt in Tabellen.
|
||||
- Alle Marktmanagement-Writes laufen über den owning Modul-Service.
|
||||
|
||||
## 4. Referenzdaten
|
||||
|
||||
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.
|
||||
|
||||
## 5. Technische Änderungsregeln
|
||||
|
||||
- Tabellen- und Constraint-Änderungen erfolgen ausschließlich über versionierte
|
||||
Migrationen.
|
||||
- Fachliche Constraints werden im DB- und Prozessvertrag gespiegelt.
|
||||
- Aggregierte Umsatzwerte werden nicht als unabhängige Fachwahrheit gespeichert.
|
||||
- Archivierte Datensätze bleiben erhalten.
|
||||
- Markt und Durchführung besitzen getrennte Archivstatus.
|
||||
@@ -0,0 +1,341 @@
|
||||
# Hermes: Market-Management-MCP-API
|
||||
|
||||
Stand: 2026-09-27
|
||||
Status: Aktive technische Schnittstellendokumentation
|
||||
|
||||
Die Marktdurchführungs-Schnittstelle enthält kein Feld `travel_duration_minutes`.
|
||||
Die Reisedauer wird nicht gespeichert, gelesen, geändert oder ausgegeben.
|
||||
|
||||
## Verbindung
|
||||
|
||||
Hermes verwendet den MCP-Endpunkt der DEV-App:
|
||||
|
||||
```text
|
||||
https://erpnaurua.imhochrain.ch/api/market-management-mcp.php
|
||||
```
|
||||
|
||||
Jeder Request ist ein JSON-RPC-2.0-Request über HTTPS. Der Request benötigt:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <MARKET_MCP_TOKEN>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
Der Token wird nicht in dieser Dokumentation oder im Repository gespeichert.
|
||||
|
||||
## Verbindungsablauf
|
||||
|
||||
## Dokumentlinks in Marktdurchführungen
|
||||
|
||||
Wenn Hermes Informationen aus einer Datei verarbeitet und diese in einer
|
||||
Marktdurchführung speichert, muss Hermes die Quelldatei als Link im Feld
|
||||
`notes` aufnehmen. Der bestehende Text darf erhalten bleiben; die Position im
|
||||
Freitext ist frei wählbar.
|
||||
|
||||
Der Link verwendet dieses Format:
|
||||
|
||||
```text
|
||||
<a href="https://erpnaurua.imhochrain.ch/files.php?path=<RFC-3986-url-encodierter-relativer-Pfad>">Dokumentname</a>
|
||||
```
|
||||
|
||||
Der Parameter `path` ist relativ zum Ordner `Hermes Naurua` und enthält nicht
|
||||
den lokalen Mount-Prefix und nicht den Ordnernamen `Hermes Naurua` selbst.
|
||||
Beispiel:
|
||||
|
||||
```text
|
||||
Quelldatei: Hermes Naurua/Marktmanagement/2026 Bellevue Noël/Standmietvertrag_Naurua.pdf
|
||||
Link: <a href="https://erpnaurua.imhochrain.ch/files.php?path=Marktmanagement%2F2026%20Bellevue%20No%C3%ABl%2FStandmietvertrag_Naurua.pdf">Standmietvertrag_Naurua.pdf</a>
|
||||
```
|
||||
|
||||
Leerzeichen, Umlaute und sonstige Sonderzeichen im URL-Parameter müssen
|
||||
vollständig URL-encodiert werden. Im Feld `notes` sind ausschließlich `<a>`
|
||||
für solche internen Dokumentlinks sowie `<br>` und `<strong>` erlaubt. Andere
|
||||
HTML-Tags und nicht erlaubte URL-Schemata werden entfernt. Externe `http://`-
|
||||
und `https://`-Links sind erlaubt und öffnen in einem neuen Tab. Der Link ist
|
||||
schreibgeschützt und erfordert bei internen Dokumenten eine aktive ERP-Anmeldung.
|
||||
Hermes muss diesen HTML-String unverändert als Feldwert senden: keine Markdown-
|
||||
Notation, keine Backticks und keine HTML-Entity-Kodierung wie `<`, `>` oder
|
||||
`"`. Der MCP-Transport erwartet den HTML-Inhalt im JSON-String und escaped
|
||||
ihn nicht als HTML.
|
||||
|
||||
Die Route benötigt auf dem ERP-Webserver die Konfiguration
|
||||
`HERMES_DOCUMENT_ROOT=/volume1/Hermes/Hermes Naurua`. Dieser lokale Pfad ist
|
||||
die auf DEV verifizierte Ablage der Synology-Freigabe.
|
||||
|
||||
## Fachliche Begrifflichkeit für Hermes
|
||||
|
||||
Die fachliche Hierarchie lautet:
|
||||
|
||||
```text
|
||||
Markt (market_definition)
|
||||
└── Marktdurchführung (market_occurrence)
|
||||
└── Markttag / Markttage (market_occurrence_day)
|
||||
```
|
||||
|
||||
Ein Markt ist die zentrale Marktdefinition. Eine Marktdurchführung ist eine
|
||||
konkrete Ausgabe bzw. Teilnahme an diesem Markt. Eine Marktdurchführung besitzt
|
||||
mindestens einen Markttag. OTC-Verkäufe werden immer einem konkreten Markttag
|
||||
zugeordnet; die zugehörige Marktdurchführung und der Markt ergeben sich aus
|
||||
dieser Referenz.
|
||||
|
||||
### 1. Initialisieren
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"protocolVersion": "2025-03-26"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Werkzeuge laden
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Werkzeug aufrufen
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "market_management.list_categories",
|
||||
"arguments": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Das Ergebnis steht bei `tools/call` als JSON im Textinhalt des ersten
|
||||
`content`-Elements.
|
||||
|
||||
## Verfügbare Werkzeuge
|
||||
|
||||
### Marktdefinitionen
|
||||
|
||||
- `market_management.search_markets`
|
||||
- optional: `search`, `category_id`, `status`, `place`,
|
||||
`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`, `organizer_id`
|
||||
- optional: `website_url` als HTTP(S)-URL des Marktes; Eingaben ohne Schema werden als `https://` normalisiert
|
||||
- `market_management.list_market_organizers`
|
||||
- liefert aktive Veranstalterorganisationen
|
||||
- `market_management.create_market_organizer`
|
||||
- erforderlich: `name`
|
||||
- optional: `email`, `phone`
|
||||
- `market_management.update_market`
|
||||
- erforderlich: `market_id`
|
||||
- änderbar: `name`, `category_id`, `website_url`
|
||||
- `market_management.archive_market`
|
||||
- erforderlich: `market_id`
|
||||
- `market_management.reactivate_market`
|
||||
- erforderlich: `market_id`
|
||||
|
||||
### Kategorien
|
||||
|
||||
- `market_management.list_categories`
|
||||
|
||||
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 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 Marktdurchführungslisten
|
||||
|
||||
### Marktdurchführungen
|
||||
|
||||
- `market_management.create_occurrence`
|
||||
- erforderlich: `market_definition_id`, `days` mit mindestens einem Markttag
|
||||
- initialer Status: `open`
|
||||
- weitere Marktdurchführungsfelder sind optional und werden typisiert validiert
|
||||
- legt automatisch eine Standard-Checkliste mit den vier festen Kapiteln an
|
||||
- `market_management.get_occurrence`
|
||||
- erforderlich: `occurrence_id`
|
||||
- liefert zusätzlich den Markt, Markttage, Kosten und
|
||||
berechnete Tages- und Gesamterlöse sowie `sales_items` je Markttag
|
||||
- `market_management.get_occurrence_checklist`
|
||||
- erforderlich: `occurrence_id`
|
||||
- liefert die vier festen Kapitel, die Punkte mit `chapter_key` und den
|
||||
Checklistenkommentar
|
||||
- `market_management.update_occurrence_checklist`
|
||||
- erforderlich: `occurrence_id`, `items`
|
||||
- ersetzt die vollständige Checkliste atomar
|
||||
- `items` enthält `chapter_key`, `label` und `is_completed`; `comment` ist optional und nullable
|
||||
- feste Kapitel: `products_sales` (Produkte & Verkauf), `decoration` (Dekoration),
|
||||
`stand_equipment` (Stand & Ausstattung), `technology` (Technik)
|
||||
- Kapitel sind nicht editierbar; Hermes darf Punkte ergänzen, entfernen,
|
||||
umbenennen, abhaken und zurücksetzen
|
||||
- `market_management.update_occurrence`
|
||||
- erforderlich: `occurrence_id`
|
||||
- änderbar: sämtliche editierbaren GUI-Marktdurchführungsfelder einschließlich
|
||||
Gemeinde, Kontaktperson, Kontakt-E-Mail, Kontakt-Telefon und `registration_note`
|
||||
- der vom Markt geerbte Veranstalter ist nicht editierbar
|
||||
|
||||
Das optionale Feld `registration_note` speichert den Freitext-Hinweis zur
|
||||
Anmeldung einer Marktdurchführung. Es wird bei `create_occurrence`,
|
||||
`get_occurrence` und `update_occurrence` als nullable String verarbeitet.
|
||||
- `market_management.archive_occurrence`
|
||||
- erforderlich: `occurrence_id`
|
||||
- `market_management.reactivate_occurrence`
|
||||
- erforderlich: `occurrence_id`
|
||||
|
||||
Unterstützte Statuswerte:
|
||||
|
||||
```text
|
||||
open
|
||||
waitlist
|
||||
registered
|
||||
confirmation_pending
|
||||
confirmed
|
||||
cancelled
|
||||
completed
|
||||
```
|
||||
|
||||
Unterstützte Zahlungsstatuswerte:
|
||||
|
||||
```text
|
||||
unpaid
|
||||
paid
|
||||
```
|
||||
|
||||
### Markttage
|
||||
|
||||
- `market_management.create_day`
|
||||
- legt einen Markttag an
|
||||
- erforderlich: `occurrence_id`, `date`
|
||||
- `market_management.update_day`
|
||||
- erforderlich: `day_id`
|
||||
- `market_management.delete_day`
|
||||
- erforderlich: `day_id`
|
||||
|
||||
Weitere mögliche Tagesfelder sind `stand_start_time`, `stand_end_time`,
|
||||
`setup_start_time`, `setup_end_time`, `delivery_start_time`,
|
||||
`delivery_end_time` und `observations`.
|
||||
|
||||
### Veranstalter und Kosten
|
||||
|
||||
- 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
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 10,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "market_management.create_market",
|
||||
"arguments": {
|
||||
"name": "Beispielmarkt Hermes Test",
|
||||
"category_id": 1,
|
||||
"idempotency_key": "hermes-create-market-001"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Beispiel: Marktdurchführung mit Markttag anlegen
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 11,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "market_management.create_occurrence",
|
||||
"arguments": {
|
||||
"market_definition_id": 1,
|
||||
"status": "open",
|
||||
"place": "Beispielort",
|
||||
"registration_deadline": "2026-10-01",
|
||||
"idempotency_key": "hermes-create-occurrence-001",
|
||||
"days": [
|
||||
{
|
||||
"date": "2026-11-21",
|
||||
"stand_start_time": "09:00",
|
||||
"stand_end_time": "18:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 12,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "market_management.create_day",
|
||||
"arguments": {
|
||||
"occurrence_id": 1,
|
||||
"date": "2026-11-21",
|
||||
"stand_start_time": "09:00",
|
||||
"stand_end_time": "18:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fachliche Grenzen
|
||||
|
||||
- Hermes erhält keinen direkten Datenbankzugriff.
|
||||
- Hermes darf keine Kategorien anlegen.
|
||||
- Umsatz und Artikelmengen werden nicht über MCP geschrieben; sie werden aus
|
||||
den OTC-Verkäufen abgeleitet.
|
||||
- `get_occurrence` liefert `sales_items` mit `day_id`, `date`, `product_id`,
|
||||
`sku`, `product_name`, `quantity`, `gross_revenue` und `unit_price`.
|
||||
- Markt und Marktdurchführung werden unabhängig archiviert.
|
||||
- Alle Schreibvorgänge werden serverseitig validiert.
|
||||
- Jeder schreibende MCP-Aufruf benötigt `idempotency_key`; das Feld ist in den
|
||||
Schemas aller Schreibwerkzeuge als Pflichtfeld deklariert. Optional kann
|
||||
`client_id` mitgegeben werden. Wiederholungen liefern dasselbe Ergebnis.
|
||||
- Die gelieferten Verkaufspositionen stammen ausschließlich aus OTC-Verkäufen,
|
||||
die einem konkreten Markttag zugeordnet sind.
|
||||
|
||||
## Fehler
|
||||
|
||||
Bei fehlender oder ungültiger Authentifizierung antwortet der Endpunkt mit
|
||||
HTTP 401. Ungültige JSON-RPC-Requests oder fachlich ungültige Eingaben werden
|
||||
als JSON-RPC-Fehlerantwort zurückgegeben.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Market Management: Hermes-MCP-Konzept
|
||||
|
||||
Stand: 2026-09-19
|
||||
Status: Verbindliches Schnittstellenkonzept
|
||||
|
||||
## 1. Zweck
|
||||
|
||||
Die App stellt einen geschützten MCP-Server bereit. Hermes Agent ist MCP-Client
|
||||
und arbeitet im Benutzerkontext. Hermes kann Marktmanagement-Daten suchen,
|
||||
anlegen, ändern, archivieren und reaktivieren.
|
||||
|
||||
## 2. Verantwortungen
|
||||
|
||||
Die App kontrolliert:
|
||||
|
||||
- Authentifizierung
|
||||
- Benutzer- und Modulberechtigung
|
||||
- Eingabevalidierung
|
||||
- fachliche Feldzuordnung
|
||||
- Persistierung
|
||||
- Audit- und Herkunftsdaten
|
||||
- Idempotenz
|
||||
|
||||
Hermes besitzt keinen direkten Zugriff auf Datenbank, interne Services oder
|
||||
fremde Module.
|
||||
|
||||
## 3. Schnittstellenmodell
|
||||
|
||||
Der MCP-Server veröffentlicht fachliche Tools, keine generischen Datenbank-
|
||||
oder Tabellenoperationen.
|
||||
|
||||
Vorgesehene Toolgruppen:
|
||||
|
||||
- Märkte suchen und lesen
|
||||
- Märkte anlegen, ändern, archivieren und reaktivieren
|
||||
- Marktdurchführungen suchen und lesen
|
||||
- Marktdurchführungen anlegen, ändern, archivieren und reaktivieren
|
||||
- Markttage anlegen, ändern und entfernen
|
||||
- Checklisten einer Marktdurchführung lesen und vollständig ändern
|
||||
- Kategorien, Kantone und Gemeinden lesen
|
||||
- Kosten und Zahlungsstatus ändern
|
||||
- Übersichtsdaten lesen
|
||||
|
||||
Hermes darf bestehende Kategorien verwenden, aber keine Kategorien anlegen.
|
||||
Umsatz- und Artikelmengen werden nicht über Hermes geschrieben, sondern aus den
|
||||
OTC-Verkäufen abgeleitet.
|
||||
|
||||
## 4. Authentifizierung und Autorisierung
|
||||
|
||||
- MCP-Endpunkt über geschütztes HTTPS
|
||||
- Bearer-Token-Prüfung an der Transportgrenze
|
||||
- der konfigurierte Token identifiziert den technischen Hermes-Benutzerkontext
|
||||
- zusätzliche Prüfung im jeweiligen Tool-Handler
|
||||
- kein direkter Datenbankzugriff für Hermes
|
||||
|
||||
Für die erste Implementierungsstufe wird bewusst keine neue OAuth-/OIDC- oder
|
||||
Entitlement-Infrastruktur eingeführt. Der Token wird ausschließlich über die
|
||||
geschützte App-Konfiguration bereitgestellt und mit konstanter Zeit verglichen.
|
||||
Die Implementierung verwendet dafür den Konfigurationswert
|
||||
`MARKET_MCP_TOKEN`.
|
||||
|
||||
Die fachliche Ownership der Lizenz-/Entitlement-Daten bleibt offen. Die
|
||||
Schnittstelle prüft nur die über den bestehenden Zugriffskontext gelieferte
|
||||
Modulberechtigung.
|
||||
|
||||
## 5. Schreibregeln
|
||||
|
||||
- Jeder Tool-Aufruf ist atomar.
|
||||
- Fehlgeschlagene Aufrufe erzeugen keine Teilpersistierung.
|
||||
- Alle Schreibvorgänge laufen über die interne Modul-API.
|
||||
- Checklisten-Schreibvorgänge ersetzen die vollständige Checkliste atomar. Hermes darf Punkte ergänzen, entfernen, umbenennen, abhaken und zurücksetzen sowie den Checklistenkommentar ändern.
|
||||
- Hermes darf bei einem neuen Markt nur eine bestehende Kategorie referenzieren.
|
||||
- Wenn Hermes Informationen aus einer Datei verarbeitet und in einer
|
||||
Marktdurchführung speichert, muss die Quelldatei als interner `<a>`-Link im
|
||||
Feld `notes` referenziert werden. Zusätzlich sind in mehrzeiligen Feldern
|
||||
`<br>` und `<strong>` erlaubt. Die Position im bestehenden Freitext ist frei
|
||||
wählbar. Externe `http://`- und `https://`-Links sind zulässig und öffnen in
|
||||
einem neuen Tab; andere HTML-Tags und URL-Schemata sind nicht zulässig. Hermes
|
||||
sendet den HTML-String unverändert; Markdown, Backticks und HTML-Entities wie
|
||||
`<`, `>` oder `"` werden nicht verwendet. Der MCP-Transport escaped
|
||||
den HTML-Inhalt nicht.
|
||||
- Unsichere Erkennung wird nicht durch eine zusätzliche App-Freigabelogik
|
||||
behandelt; die fachliche Qualitätssicherung liegt im Hermes-Training.
|
||||
|
||||
## 6. Wiederholungsschutz
|
||||
|
||||
Schreibende Aufrufe können eine Idempotenzreferenz übergeben. Die App speichert
|
||||
Client, Benutzer, Tool, Schlüssel, Request-Hash und Verarbeitungsergebnis in
|
||||
`market_mcp_idempotency`.
|
||||
|
||||
Ein wiederholter Aufruf mit gleichem Schlüssel und gleichem Request liefert das
|
||||
bereits bekannte Ergebnis. Ein gleicher Schlüssel mit verändertem Request wird
|
||||
abgewiesen.
|
||||
|
||||
## 7. Daten- und Modulgrenzen
|
||||
|
||||
- Marktmanagement schreibt nur in eigene Tabellen.
|
||||
- Das OTC-Modul schreibt die Verkaufstabelle und referenziert den
|
||||
`market_occurrence_day_id`.
|
||||
- Kontakt- und Organisationsdaten werden über das Kontaktmodul referenziert.
|
||||
- Gemeindedaten werden über den eigenen Referenzbestand gelesen.
|
||||
- MCP-Komposition enthält keine Marktfachlogik.
|
||||
@@ -0,0 +1,414 @@
|
||||
# Market Management: Normatives Tabellen- und Prozesskonzept
|
||||
|
||||
Stand: 2026-09-19
|
||||
Status: Verbindliches Konzept für das Modul `market_management`
|
||||
|
||||
## 1. Übersicht
|
||||
|
||||
Tabellen:
|
||||
|
||||
- `market_category`
|
||||
- `market_definition`
|
||||
- `market_occurrence`
|
||||
- `market_occurrence_organizer`
|
||||
- `market_occurrence_day`
|
||||
- `market_occurrence_cost_item`
|
||||
- `market_occurrence_checklist`
|
||||
- `market_occurrence_checklist_item`
|
||||
- `market_municipality_reference`
|
||||
- `market_municipality`
|
||||
- `market_mcp_idempotency`
|
||||
|
||||
Bestehende Integrationsreferenz:
|
||||
|
||||
- `sales_order.market_occurrence_day_id` im owning OTC-/Direktverkaufsmodul
|
||||
|
||||
Prozessbereiche:
|
||||
|
||||
- zentrale Marktdefinition verwalten
|
||||
- Durchführung verwalten
|
||||
- Durchführungstage verwalten
|
||||
- Kosten und Zahlungsstatus verwalten
|
||||
- OTC-Verkäufe einem Durchführungstag zuordnen
|
||||
- Umsatz- und Artikelmengen lesen und aggregieren
|
||||
- Marktübersicht lesen
|
||||
- Hermes-MCP-Zugriff ausführen
|
||||
- Marktdurchführungs-Checkliste verwalten
|
||||
- Referenzdaten verwalten
|
||||
|
||||
Die Checkliste gehört zur Marktdurchführung, nicht zum einzelnen Markttag.
|
||||
Sie besitzt die vier festen Kapitel `Produkte & Verkauf`, `Dekoration`,
|
||||
`Stand & Ausstattung` und `Technik`. Kapitel und Kapitelreihenfolge sind
|
||||
fest; Punkte innerhalb eines Kapitels bleiben für Benutzer und Hermes
|
||||
editierbar. Der Kommentar gehört zur gesamten Checkliste.
|
||||
|
||||
## 2. Zweck und Scope
|
||||
|
||||
Dieses Dokument beschreibt das normative technische Zielbild für die Umsetzung
|
||||
bis einschließlich Punkt 10 des Anforderungsprofils.
|
||||
|
||||
Enthalten sind:
|
||||
|
||||
- zentrale Marktdefinition mit eindeutigem Namen und genau einer Kategorie
|
||||
- beliebig viele Durchführungen je Markt
|
||||
- mindestens ein Durchführungstag je Durchführung
|
||||
- veranstaltungs-, tages-, kosten- und zahlungsbezogene Daten
|
||||
- OTC-basierte Umsatz- und Artikelmengenermittlung
|
||||
- Archivierung von Markt und Durchführung auf getrennten Ebenen
|
||||
- serverseitige Übersichtsdaten
|
||||
- Benutzer- und Modulberechtigung
|
||||
- Hermes als MCP-Client der App
|
||||
|
||||
Nicht enthalten sind Punkt 11 und folgende, insbesondere intelligente
|
||||
Auswertungen, Korrelationen und wirtschaftliche Gemeindedaten.
|
||||
|
||||
GUI-Details, konkrete HTTP-Komposition und technische Hermes-Implementierung
|
||||
werden in den zugehörigen Detaildokumenten beschrieben.
|
||||
|
||||
## 3. Normatives Datenmodell
|
||||
|
||||
```text
|
||||
Kategorie / Marktart
|
||||
└── Marktdefinition
|
||||
└── Durchführung
|
||||
└── Durchführungstag / Subdurchführung
|
||||
└── OTC-Verkäufe
|
||||
```
|
||||
|
||||
### 3.1 `market_category`
|
||||
|
||||
Deterministische Kategorie-/Marktartliste.
|
||||
|
||||
Normativ:
|
||||
|
||||
- Kategorienamen sind eindeutig.
|
||||
- Fabienne darf die Liste erweitern.
|
||||
- Hermes darf bestehende Kategorien auswählen, aber keine Kategorien anlegen.
|
||||
- Deaktivierte Kategorien bleiben für historische Zuordnungen erhalten.
|
||||
|
||||
### 3.2 `market_definition`
|
||||
|
||||
Zentrale fachliche Marktdefinition.
|
||||
|
||||
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
|
||||
- Markt und Durchführungen werden getrennt archiviert und reaktiviert
|
||||
|
||||
### 3.3 `market_occurrence`
|
||||
|
||||
Konkrete Teilnahme bzw. Veranstaltungsausgabe eines Marktes.
|
||||
|
||||
Normativ:
|
||||
|
||||
- jede Durchführung gehört genau einer Marktdefinition
|
||||
- jede Durchführung besitzt mindestens einen Durchführungstag
|
||||
- variable Veranstaltungs-, Kontakt-, Orts-, Logistik-, Kosten- und
|
||||
Zahlungsdaten gehören zur Durchführung
|
||||
- der Status gehört zur Durchführung
|
||||
- Zahlungsstatus und Marktstatus sind unabhängig
|
||||
- der optionale `registration_note` gehört als Freitext zur Durchführung
|
||||
- der Veranstalter wird von jeder Durchführung des Marktes geerbt und ist dort nicht editierbar
|
||||
- der Zahlungsstatus ist manuell `paid` oder `unpaid`
|
||||
|
||||
Minimale Pflichtfelder über alle Statuswerte hinweg:
|
||||
|
||||
- `market_definition_id`
|
||||
- initialer Status `open`
|
||||
- mindestens ein Durchführungstag
|
||||
|
||||
### 3.4 `market_occurrence_organizer`
|
||||
|
||||
Veranstalterbeziehung einer Durchführung.
|
||||
|
||||
Normativ:
|
||||
|
||||
- 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`
|
||||
|
||||
Einzelner Durchführungstag bzw. Subdurchführung.
|
||||
|
||||
Normativ:
|
||||
|
||||
- jeder Tag gehört genau einer Durchführung
|
||||
- das Datum ist je Durchführung eindeutig
|
||||
- Stand-, Aufbau-, Abbau- und tagesbezogene Anlieferungszeiten gehören hierher
|
||||
- Ereignisse und Beobachtungen gehören hierher
|
||||
- OTC-Verkäufe werden eindeutig diesem Datensatz zugeordnet
|
||||
- Bruttoerlös und Artikelmenge werden aus den zugeordneten OTC-Verkäufen
|
||||
abgeleitet und nicht manuell gespeichert
|
||||
|
||||
Minimale Pflichtfelder:
|
||||
|
||||
- `occurrence_id`
|
||||
- `date`
|
||||
|
||||
### Mindestpflichtfelder
|
||||
|
||||
Über alle Statuswerte hinweg gelten nur diese Pflichtfelder:
|
||||
|
||||
- Marktdefinition: `name`, `category_id`; `website_url` ist optional
|
||||
- Durchführung: `market_definition_id`, initialer Status `open`, mindestens ein
|
||||
Durchführungstag
|
||||
- Durchführungstag: `occurrence_id`, `date`
|
||||
|
||||
Alle weiteren Felder bleiben bei der Anlage optional und werden unabhängig vom
|
||||
Status ergänzt.
|
||||
|
||||
### 3.6 Kosten einer Durchführung
|
||||
|
||||
Die Durchführung besitzt einen einfachen Gesamtkostenbetrag und eine optionale
|
||||
mehrzeilige Freitext-Aufschlüsselung der Kostenpositionen.
|
||||
|
||||
Normativ:
|
||||
|
||||
- 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
|
||||
- Netto- und Steuerbetrag werden aus Brutto und Steuerart/-satz berechnet
|
||||
- die Freitext-Aufschlüsselung wird als Beschreibung gespeichert
|
||||
|
||||
### 3.7 `market_municipality_reference`
|
||||
|
||||
Technischer Referenzbestand des offiziellen Schweizer Gemeindeverzeichnisses.
|
||||
|
||||
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.9 `market_mcp_idempotency`
|
||||
|
||||
Technischer Idempotenzbestand für wiederholte Hermes-Schreibaufrufe.
|
||||
|
||||
Normativ:
|
||||
|
||||
- derselbe Client und Idempotenzschlüssel erzeugen keine doppelte Wirkung
|
||||
- ein abweichender Request mit demselben Schlüssel wird abgewiesen
|
||||
- der gespeicherte Verarbeitungsausgang bleibt nachvollziehbar
|
||||
|
||||
### 3.9 OTC-Integration
|
||||
|
||||
Das OTC-Modul bleibt Owner seiner Verkaufstabellen. Jeder neue OTC-Verkauf
|
||||
referenziert eindeutig einen `market_occurrence_day_id`. Markt, Durchführung,
|
||||
Datum, Umsatz und Artikelmengen werden daraus ermittelt.
|
||||
|
||||
Historische OTC-Verkäufe ohne diese Zuordnung werden nicht migriert.
|
||||
|
||||
## 4. Normatives Prozessmodell
|
||||
|
||||
Alle fachlichen Prozesse gehören zum owning Modul `market_management`, außer der
|
||||
OTC-Verkaufsprozess selbst. Die OTC-Integration erfolgt ausschließlich über die
|
||||
öffentliche Marktmanagement-Schnittstelle.
|
||||
|
||||
### 4.1 Zentrale Marktdefinition verwalten
|
||||
|
||||
Fachliche Aufgabe:
|
||||
|
||||
- Marktdefinition mit eindeutigem Namen und Kategorie anlegen, ändern,
|
||||
archivieren oder reaktivieren
|
||||
|
||||
Liest:
|
||||
|
||||
- Kategorien
|
||||
- bestehende Marktdefinitionen
|
||||
|
||||
Schreibt:
|
||||
|
||||
- `market_category`, sofern Fabienne eine Kategorie erweitert
|
||||
- `market_definition`
|
||||
|
||||
### 4.2 Durchführung verwalten
|
||||
|
||||
Fachliche Aufgabe:
|
||||
|
||||
- eine konkrete Durchführung eines Marktes anlegen, bearbeiten, archivieren
|
||||
oder reaktivieren
|
||||
|
||||
Liest:
|
||||
|
||||
- Marktdefinition
|
||||
- Kontaktmodul
|
||||
- Gemeindereferenz
|
||||
|
||||
Schreibt:
|
||||
|
||||
- `market_occurrence`
|
||||
- `market_occurrence_organizer`
|
||||
|
||||
### 4.3 Durchführungstage verwalten
|
||||
|
||||
Fachliche Aufgabe:
|
||||
|
||||
- Durchführungstage anlegen, bearbeiten und entfernen
|
||||
|
||||
Liest:
|
||||
|
||||
- Durchführung
|
||||
|
||||
Schreibt:
|
||||
|
||||
- `market_occurrence_day`
|
||||
|
||||
### 4.4 Kosten und Zahlungsstatus verwalten
|
||||
|
||||
Fachliche Aufgabe:
|
||||
|
||||
- Gesamtkosten, Kostenaufschlüsselung und manuelle Zahlungsinformationen pflegen
|
||||
|
||||
Liest:
|
||||
|
||||
- Durchführung
|
||||
- bestehende Kosteninformation
|
||||
|
||||
Schreibt:
|
||||
|
||||
- technische Standkosten-Position in `market_occurrence_cost_item`
|
||||
- Zahlungsfelder in `market_occurrence`
|
||||
|
||||
### 4.5 OTC-Verkauf einem Durchführungstag zuordnen
|
||||
|
||||
Fachliche Aufgabe:
|
||||
|
||||
- beim OTC-Verkauf einen gültigen Durchführungstag auswählen und referenzieren
|
||||
|
||||
Liest:
|
||||
|
||||
- verfügbare Märkte, Durchführungen und Durchführungstage über die
|
||||
Marktmanagement-Schnittstelle
|
||||
|
||||
Schreibt:
|
||||
|
||||
- ausschließlich das OTC-Modul in seine eigene Verkaufstabelle
|
||||
|
||||
### 4.6 Umsatz- und Artikelmengen lesen
|
||||
|
||||
Fachliche Aufgabe:
|
||||
|
||||
- Umsatz und Artikelmengen für Tag, Durchführung und Markt aus den OTC-Daten
|
||||
aggregieren
|
||||
|
||||
Liest:
|
||||
|
||||
- zugeordnete OTC-Verkäufe und Verkaufspositionen
|
||||
- Markt-, Durchführungs- und Tagesdaten
|
||||
|
||||
Schreibt:
|
||||
|
||||
- keine fachlichen Umsatzsummen; die Werte sind abgeleitet
|
||||
|
||||
### 4.7 Marktübersicht lesen
|
||||
|
||||
Fachliche Aufgabe:
|
||||
|
||||
- Märkte, Durchführungen und Tage serverseitig suchen, filtern, sortieren und
|
||||
aggregiert ausgeben
|
||||
|
||||
Liest:
|
||||
|
||||
- alle owned Marktmanagement-Daten
|
||||
- Kontakt- und Gemeindereferenzen
|
||||
- OTC-Aggregate
|
||||
|
||||
Schreibt:
|
||||
|
||||
- nichts
|
||||
|
||||
### 4.8 Hermes-MCP-Zugriff
|
||||
|
||||
Fachliche Aufgabe:
|
||||
|
||||
- Hermes über die kontrollierte MCP-Schnittstelle suchen, anlegen, ändern,
|
||||
archivieren und reaktivieren lassen
|
||||
|
||||
Liest:
|
||||
|
||||
- Marktmanagement-Daten und erlaubte Referenzdaten
|
||||
|
||||
Schreibt:
|
||||
|
||||
- ausschließlich über die internen Marktmanagement-Services
|
||||
- `market_mcp_idempotency` für Wiederholungsschutz
|
||||
|
||||
Die konkrete MCP-Schnittstelle ist in
|
||||
[market_management_hermes_mcp_konzept.md](market_management_hermes_mcp_konzept.md)
|
||||
dokumentiert.
|
||||
|
||||
## 5. Normative Betriebsregeln
|
||||
|
||||
- Marktname und Kategorie sind zentrale Stammdaten.
|
||||
- Alle variablen Veranstaltungsdaten werden je Durchführung gepflegt.
|
||||
- Tageswerte werden je Durchführungstag gepflegt.
|
||||
- Wochentag, Saison, Datumsbereich, Gesamtkosten, offener Betrag, Umsatz und
|
||||
Artikelmengen werden abgeleitet.
|
||||
- Umsatz und Artikelmengen stammen ausschließlich aus OTC-Verkäufen.
|
||||
- Stornos und Rückerstattungen werden in den Aggregaten abgezogen.
|
||||
- Markt- und Durchführungarchivierung sind unabhängig.
|
||||
- Der erste Status ist `offen`.
|
||||
- `durchgefuehrt` wird nach dem letzten Durchführungstag automatisch gesetzt,
|
||||
sofern die Durchführung nicht `abgesagt` ist.
|
||||
- Die GUI enthält keine fachliche Primärlogik.
|
||||
- Hermes erhält keinen direkten DB-Zugriff.
|
||||
- Punkt 11 und folgende sind nicht Teil dieses Zielbilds.
|
||||
|
||||
## 6. Ausgabevertrag
|
||||
|
||||
Das Modul liefert:
|
||||
|
||||
- eindeutige Markt-, Durchführungs- und Tagesidentitäten
|
||||
- validierte Stammdaten und Referenzen
|
||||
- unabhängigen Markt- und Zahlungsstatus
|
||||
- berechnete Kosten-, Umsatz- und Artikelmengenwerte
|
||||
- serverseitig filter- und sortierbare Übersichtsdaten
|
||||
- kontrollierte MCP-Ergebnisse für Hermes
|
||||
|
||||
## 7. Erfolgskriterien
|
||||
|
||||
- Marktdefinitionen sind eindeutig und genau einer Kategorie zugeordnet.
|
||||
- Durchführungen und Durchführungstage sind unabhängig und vollständig
|
||||
verwaltbar.
|
||||
- Jeder neue OTC-Verkauf ist genau einem Durchführungstag zugeordnet.
|
||||
- Umsatz und Artikelmengen werden nicht manuell doppelt erfasst.
|
||||
- Kontakt- und Gemeindedaten werden über definierte Referenzen verwendet.
|
||||
- Archivierung funktioniert getrennt für Markt und Durchführung.
|
||||
- MCP-Schreibvorgänge sind authentifiziert, berechtigt und idempotent.
|
||||
- Die fachliche Logik bleibt im owning Modul.
|
||||
|
||||
## 8. Noch nicht normative Detailverträge
|
||||
|
||||
Die folgenden Dokumente werden erst nach expliziter Prozessdefinition als
|
||||
normative Verträge erstellt:
|
||||
|
||||
- Hauptprozesse unter `processes/main/`
|
||||
- Sub-Prozesse unter `processes/sub_processes/`
|
||||
- konkrete API-Schemas der Marktmanagement-Schnittstelle
|
||||
- GUI- und View-Verträge
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
# market_management.occurrence_status_completion
|
||||
Stand: 2026-09-19
|
||||
Status: Verbindliche Prozess-Spezifikation
|
||||
|
||||
## 1. Zweck
|
||||
|
||||
Setzt aktive Durchführungen am täglichen Cron-Lauf automatisch auf
|
||||
`completed`, sobald der Kalendertag nach ihrem letzten Durchführungstag erreicht
|
||||
ist. Abgesagte Durchführungen werden nicht verändert.
|
||||
|
||||
## 2. Prozess-Einbettung
|
||||
|
||||
Täglicher synchroner Cron-Teilprozess des zentralen System-Cron-Entrypoints.
|
||||
Die fachliche Statusänderung gehört zum owning Modul `market_management`.
|
||||
|
||||
## 3. Input
|
||||
|
||||
- aktuelles Datum in der Zeitzone `Europe/Zurich`
|
||||
- keine vorbereiteten Fachdaten; der Prozess liest seine Daten selbst aus der
|
||||
Datenbank
|
||||
|
||||
## 4. Exakter Prozessablauf
|
||||
|
||||
1. Einen technischen Lauf in `public.process_runs` mit dem aktuellen Datum
|
||||
starten.
|
||||
2. Aus `market_occurrence` und `market_occurrence_day` aktive Durchführungen
|
||||
mit mindestens einem Tag bestimmen.
|
||||
3. Durchführungen ausschliessen, deren Status `cancelled` oder bereits
|
||||
`completed` ist.
|
||||
4. Für jede verbleibende Durchführung den letzten Durchführungstag bestimmen.
|
||||
5. Wenn dieser Tag vor dem aktuellen Datum liegt, den Status atomar auf
|
||||
`completed` setzen.
|
||||
6. Den technischen Lauf mit Anzahl und IDs der geänderten Durchführungen
|
||||
abschliessen.
|
||||
|
||||
Gelesen werden `market_occurrence` und `market_occurrence_day`. Geschrieben
|
||||
werden `market_occurrence.status`, `market_occurrence.updated_at` und der
|
||||
technische Lauf in `public.process_runs`.
|
||||
|
||||
## 5. Batch, Betriebsmodell und Einbettung
|
||||
|
||||
Ein täglicher synchroner Cron-Lauf; ein SQL-Update verarbeitet den vollständigen
|
||||
aktuellen Scope atomar. Die bestehende Cron-Sperre verhindert parallele Läufe.
|
||||
|
||||
## 6. Output
|
||||
|
||||
Ein kompakter Laufstatus mit aktuellem Datum, Anzahl und IDs der auf
|
||||
`completed` gesetzten Durchführungen sowie der technischen Lauf-ID.
|
||||
|
||||
## 7. Erfolgskriterien
|
||||
|
||||
- Eine Durchführung wird am Tag nach ihrem letzten Durchführungstag auf
|
||||
`completed` gesetzt.
|
||||
- Abgesagte und bereits abgeschlossene Durchführungen bleiben unverändert.
|
||||
- Durchführungen ohne Durchführungstag werden nicht verändert.
|
||||
- Wiederholte Cron-Läufe sind idempotent.
|
||||
- `public.process_runs` enthält einen abgeschlossenen technischen Lauf.
|
||||
|
||||
## 8. Fehlerschranke
|
||||
|
||||
Ein technischer Fehler bricht den Lauf ab, rollt die fachliche Transaktion
|
||||
zurück und markiert `process_runs` als `failed`.
|
||||
|
||||
## 9. Fachliche Betriebsregel
|
||||
|
||||
Die Prüfung erfolgt einmal täglich über den bestehenden Cron. Die Statusänderung
|
||||
ist unabhängig von GUI-Aufrufen und OTC-Bestellungen.
|
||||
|
||||
## 10. Sub-Prozess-Referenzen
|
||||
|
||||
Keine zusätzlichen Sub-Prozesse.
|
||||
|
||||
## 11. End-to-End Sub-Prozess-Kette
|
||||
|
||||
System-Cron → Statusabschlussprozess → `market_occurrence` und
|
||||
`public.process_runs`.
|
||||
|
||||
## 12. Sub-Prozess-Wiederverwendung
|
||||
|
||||
Der bestehende System-Cron-Entrypoint und die bestehende Cron-Sperre werden
|
||||
wiederverwendet. Ein zusätzlicher Scheduler wird nicht eingeführt.
|
||||
|
||||
## 13. Step-Liste
|
||||
|
||||
- Lauf starten
|
||||
- eligible Durchführungen bestimmen und abschliessen
|
||||
- Laufstatus persistieren
|
||||
|
||||
## 14. Parallelisierung
|
||||
|
||||
Keine Parallelisierung; der deterministische SQL-Batch läuft synchron und
|
||||
atomar.
|
||||
|
||||
## 15. Output-Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "done",
|
||||
"as_of_date": "YYYY-MM-DD",
|
||||
"completed_count": 0,
|
||||
"completed_occurrence_ids": [],
|
||||
"technical_run_id": 0
|
||||
}
|
||||
```
|
||||
|
||||
## 16. Done-Kriterien
|
||||
|
||||
- Der tägliche Cron ruft den Prozess genau einmal auf.
|
||||
- Die Statusregel ist serverseitig implementiert.
|
||||
- Der Prozess schreibt `process_runs` vollständig.
|
||||
- Wiederholte Läufe erzeugen keine erneuten Statusänderungen.
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
# Anforderungsprofil Marktübersicht
|
||||
|
||||
Stand: 2026-09-15
|
||||
Status: Fachliche Arbeitsanforderung
|
||||
|
||||
## 1. Ziel des Systems
|
||||
|
||||
Das System unterstützt die Suche, Prüfung, Anmeldung, Durchführung und
|
||||
Auswertung von Herbstmärkten, Weihnachtsmärkten und weiteren geeigneten
|
||||
Märkten.
|
||||
|
||||
## 2. Umfang der ersten Phase
|
||||
|
||||
- Es wird jeweils nur ein Markt gleichzeitig bearbeitet.
|
||||
- Zunächst gibt es nur eine Benutzerin: Fabienne.
|
||||
- Eine eigene Benutzerverwaltung ist zunächst nicht erforderlich.
|
||||
- Die Anwendung soll übersichtlich und einfach bedienbar sein.
|
||||
|
||||
## 3. Datenhierarchie
|
||||
|
||||
Die Daten müssen mehrstufig abbildbar sein:
|
||||
|
||||
```text
|
||||
Kategorie
|
||||
└── Markt
|
||||
└── Durchführung
|
||||
└── Durchführungstag
|
||||
```
|
||||
|
||||
- Es können beliebig viele Kategorien und Märkte existieren.
|
||||
- Ein Markt gehört genau einer Kategorie.
|
||||
- Fabienne darf die deterministische Kategorie-/Marktartliste erweitern.
|
||||
- Hermes darf bestehende Kategorien auswählen, aber keine Kategorien anlegen.
|
||||
- Ein Markt kann beliebig viele Durchführungen enthalten.
|
||||
- Eine Durchführung kann beliebig viele Durchführungstage enthalten.
|
||||
- Erlös- und Verkaufsdaten werden auf Ebene des Durchführungstags erfasst und
|
||||
auf Ebene der Durchführung sowie des Marktes auswertbar gemacht.
|
||||
|
||||
Der zentrale Marktstamm enthält ausschließlich:
|
||||
|
||||
- Marktname
|
||||
- Marktart/Kategorie aus einer deterministischen Liste
|
||||
- optionale Website-URL der Veranstaltung
|
||||
|
||||
Alle übrigen veränderlichen Angaben werden pro Durchführung erfasst. Tages-
|
||||
und Verkaufsdaten werden pro Durchführungstag erfasst.
|
||||
|
||||
## 4. Nutzungsberechtigung
|
||||
|
||||
- Die Rechte zur Nutzung des Moduls werden an die Benutzeridentität geknüpft.
|
||||
- Das Modul soll ohne die restliche Anwendung an weitere Personen verkauft und
|
||||
separat genutzt werden können.
|
||||
- Die Ownership und Ablage der Lizenz-/Entitlement-Daten ist noch nicht
|
||||
entschieden und bleibt offen.
|
||||
|
||||
## 5. Marktarten
|
||||
|
||||
Initial verfügbare Marktarten:
|
||||
|
||||
- Herbstmärkte
|
||||
- Weihnachtsmärkte
|
||||
- Frühlingsmärkte
|
||||
- Ostermärkte
|
||||
- Kunsthandwerksmärkte
|
||||
- Regionalmärkte
|
||||
- Weitere geeignete Märkte
|
||||
|
||||
Die Marktart/Kategorie wird zentral am Markt aus einer deterministischen Liste
|
||||
ausgewählt. Eine Durchführung überschreibt diese Zuordnung nicht.
|
||||
|
||||
## 6. Markt- und Durchführungsdaten
|
||||
|
||||
### 6.1 Allgemeine Informationen
|
||||
|
||||
- Veranstalter
|
||||
- Ort
|
||||
- Gemeinde
|
||||
- Kanton
|
||||
- Adresse
|
||||
- Kontaktperson
|
||||
- E-Mail-Adresse
|
||||
- Telefonnummer
|
||||
- Notizen
|
||||
|
||||
Diese Angaben werden pro Durchführung erfasst. Marktname und Marktart/Kategorie
|
||||
gehören ausschließlich zur zentralen Marktdefinition.
|
||||
|
||||
### 6.2 Termin und Durchführung
|
||||
|
||||
- Beginn und Ende des Marktes
|
||||
- Datum jedes einzelnen Markttages
|
||||
- Standzeiten pro Tag
|
||||
- Auf- und Abbauzeiten
|
||||
- Anmeldefrist
|
||||
- Datum der Anfrage oder Anmeldung
|
||||
- Datum der Bestätigung
|
||||
- Datum der Durchführung
|
||||
- Wochentag
|
||||
- Saison
|
||||
|
||||
### 6.3 Stand und Infrastruktur
|
||||
|
||||
- Standgröße
|
||||
- Tisch vorhanden
|
||||
- Tisch kann gemietet werden
|
||||
- Eigener Tisch erforderlich
|
||||
- Zelt oder Überdachung vorhanden
|
||||
- Strom vorhanden
|
||||
- Wasser vorhanden
|
||||
- Beleuchtung vorhanden
|
||||
- Weitere Infrastruktur
|
||||
- Anforderungen an den eigenen Stand
|
||||
|
||||
### 6.4 Anfahrt und Logistik
|
||||
|
||||
- Anfahrtsadresse
|
||||
- Entfernung oder Fahrzeit
|
||||
- Parkmöglichkeiten
|
||||
- Zufahrt für Anlieferung
|
||||
- Zeitfenster für die Anlieferung
|
||||
- Möglichkeit zum direkten Ausladen
|
||||
- Lager- oder Aufbewahrungsmöglichkeiten
|
||||
- Besondere Hinweise zur Anfahrt
|
||||
|
||||
### 6.5 Kosten und Konditionen
|
||||
|
||||
- Standgebühr
|
||||
- Gebühren für Tisch, Strom oder weitere Infrastruktur
|
||||
- Weitere Pflichtkosten
|
||||
- Gesamtkosten
|
||||
- Zahlungsfrist
|
||||
- Zahlungsart
|
||||
- Abmeldebedingungen
|
||||
- Umsatzbeteiligung, falls vorhanden
|
||||
- Weitere Teilnahmebedingungen
|
||||
|
||||
### 6.6 Vertrag und Unterlagen
|
||||
|
||||
Die zugehörigen Unterlagen sollen über einen Link mit einem eigenen Ordner
|
||||
verbunden werden können.
|
||||
|
||||
- Link zum Vertragsordner
|
||||
|
||||
Der Link wird pro Durchführung erfasst.
|
||||
|
||||
### 6.7 Deterministische Referenzwerte und Berechnungen
|
||||
|
||||
Deterministisch vorgegebene Referenzwerte; die jeweilige Auswahl wird auf der
|
||||
zuständigen Ebene gespeichert:
|
||||
|
||||
- Marktart/Kategorie
|
||||
- Marktstatus
|
||||
- Zahlungsstatus
|
||||
- Zahlungsart
|
||||
- Kanton
|
||||
- standardisierte Ja/Nein-Merkmale für Infrastrukturangaben
|
||||
|
||||
Abgeleitete Werte werden nicht manuell eingegeben:
|
||||
|
||||
- Wochentag aus dem Datum
|
||||
- Saison aus dem Durchführungsdatum
|
||||
- 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
|
||||
|
||||
## 7. Statusverwaltung
|
||||
|
||||
Folgende Marktstatus sollen verfügbar sein:
|
||||
|
||||
- Offen
|
||||
- Warteliste
|
||||
- Angemeldet
|
||||
- Bestätigung ausstehend
|
||||
- Bestätigt
|
||||
- Abgesagt
|
||||
- Durchgeführt
|
||||
|
||||
Der initiale Status ist `Offen`. Die Statuswerte werden grundsätzlich manuell
|
||||
gesetzt. `Durchgeführt` wird nach dem letzten Durchführungstag automatisch
|
||||
gesetzt, sofern die Durchführung nicht abgesagt ist. Manuelle Korrektur bleibt
|
||||
möglich.
|
||||
|
||||
## 8. Bezahlung
|
||||
|
||||
Der Zahlungsstatus wird unabhängig vom allgemeinen Marktstatus gespeichert.
|
||||
|
||||
Zahlungsstatus:
|
||||
|
||||
- Nicht bezahlt
|
||||
- Bezahlt
|
||||
|
||||
Zusätzliche Zahlungsinformationen:
|
||||
|
||||
- Zahlungsdatum
|
||||
- Bezahlter Betrag
|
||||
- Offener Betrag
|
||||
|
||||
## 9. Erlöserfassung
|
||||
|
||||
Der Erlös wird nicht manuell erfasst, sondern aus den OTC-Verkäufen berechnet.
|
||||
Bei mehrtägigen Märkten wird der Erlös je Durchführungstag separat ermittelt.
|
||||
|
||||
Pro Durchführungstag:
|
||||
|
||||
- Datum
|
||||
- Bruttoerlös aus OTC-Verkäufen
|
||||
- Anzahl verkaufter Artikel aus OTC-Positionen
|
||||
- Besondere Ereignisse oder Beobachtungen
|
||||
|
||||
Automatisch zu berechnen:
|
||||
|
||||
- Gesamterlös des Marktes
|
||||
- Gesamtzahl verkaufter Artikel
|
||||
- Durchschnittlicher Erlös pro Tag
|
||||
- Durchschnittlicher Erlös pro verkauftem Artikel, sofern berechenbar
|
||||
|
||||
Die Tageswerte sollen einzeln und als Gesamtergebnis auswertbar sein.
|
||||
|
||||
## 10. Marktübersicht
|
||||
|
||||
Die zentrale Übersicht soll ermöglichen:
|
||||
|
||||
- alle Märkte anzuzeigen
|
||||
- nach Marktart zu filtern
|
||||
- nach Status zu filtern
|
||||
- nach Gemeinde oder Ort zu filtern
|
||||
- nach Datum zu sortieren
|
||||
- nach Erlös zu sortieren
|
||||
- den Bezahlstatus zu erkennen
|
||||
- kommende Märkte hervorzuheben
|
||||
- überfällige Anmeldungen oder Zahlungen zu erkennen
|
||||
- Märkte zu öffnen und zu bearbeiten
|
||||
|
||||
Mindestens sichtbar:
|
||||
|
||||
- Marktname
|
||||
- Marktart
|
||||
- Ort oder Gemeinde
|
||||
- Datum
|
||||
- Wochentag
|
||||
- Status
|
||||
- Gesamtkosten
|
||||
- Bezahlstatus
|
||||
- Bruttoerlös
|
||||
|
||||
## 11. Intelligente Auswertungen
|
||||
|
||||
Das System soll später Zusammenhänge, Unterschiede und mögliche Korrelationen
|
||||
zwischen Marktmerkmalen und Ergebnissen sichtbar machen.
|
||||
|
||||
### 10.1 Marktart und Erlös
|
||||
|
||||
- Welche Marktarten erzielen den höchsten Erlös?
|
||||
- Sind Weihnachtsmärkte erfolgreicher als Herbstmärkte?
|
||||
- Welche Marktarten haben das beste Verhältnis zwischen Erlös und Kosten?
|
||||
- Welche Marktarten führen zu den meisten verkauften Artikeln?
|
||||
|
||||
### 10.2 Datum, Saison und Erlös
|
||||
|
||||
- An welchen Daten werden die höchsten Erlöse erzielt?
|
||||
- Gibt es Unterschiede zwischen frühem und spätem Saisonzeitpunkt?
|
||||
- Wie entwickelt sich der Erlös innerhalb einer Saison?
|
||||
- Welche Monate und Jahreszeiten sind besonders erfolgreich?
|
||||
- Gibt es einen Zusammenhang zwischen Marktdatum und Verkaufsleistung?
|
||||
|
||||
### 10.3 Zahlungstermin und Markterfolg
|
||||
|
||||
- Wie viele Tage liegen zwischen Zahlungstermin und Marktdatum?
|
||||
- Laufen Märkte besser, wenn der Zahlungstermin früh oder spät vor dem Markt
|
||||
liegt?
|
||||
- Gibt es einen Zusammenhang zwischen dem Zeitpunkt der Zahlung und dem
|
||||
späteren Erlös?
|
||||
- Gibt es Unterschiede zwischen Märkten vor und nach dem eigenen Zahlungstag?
|
||||
|
||||
### 10.4 Gemeinde und wirtschaftliches Umfeld
|
||||
|
||||
Der Ort soll später über die Gemeinde mit zusätzlichen wirtschaftlichen Daten
|
||||
vergleichbar sein.
|
||||
|
||||
Zu prüfen ist insbesondere die Hypothese, dass Märkte in Gemeinden mit höherem
|
||||
durchschnittlichem Einkommen möglicherweise einen höheren Erlös erzielen als
|
||||
Märkte in Gemeinden mit tieferem Einkommen.
|
||||
|
||||
Auswertungen:
|
||||
|
||||
- Zusammenhang zwischen Gemeinde und Erlös
|
||||
- Zusammenhang zwischen Gemeinde-Einkommen und Erlös
|
||||
- Zusammenhang zwischen Kaufkraft und Anzahl verkaufter Artikel
|
||||
- Vergleich verschiedener Gemeinden
|
||||
- Vergleich ähnlicher Märkte in Gemeinden mit unterschiedlichem
|
||||
Einkommensniveau
|
||||
|
||||
Die Quelle, Aktualisierung und fachliche Ownership der zusätzlichen
|
||||
wirtschaftlichen Gemeindedaten sind noch nicht spezifiziert.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Editable Action Card
|
||||
|
||||
## Zweck
|
||||
|
||||
`Editable Action Card` ist ein fachlich neutrales UI-Pattern fuer Karten mit
|
||||
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
|
||||
- gleichzeitiges Bearbeiten mehrerer Karten
|
||||
- optionales Ein- und Ausblenden des Karteninhalts
|
||||
|
||||
Fachliche Aktionen, API-Aufrufe, Validierung und die Aktualisierung fachlicher
|
||||
Daten bleiben beim owning Modul.
|
||||
|
||||
## Markup-Vertrag
|
||||
|
||||
Eine Karte wird mit `data-shared-editable-action-card` markiert. Die
|
||||
Bedienelemente verwenden folgende Attribute:
|
||||
|
||||
- `data-shared-editable-card-edit`
|
||||
- `data-shared-editable-card-cancel`
|
||||
- `data-shared-editable-card-read`
|
||||
- `data-shared-editable-card-form`
|
||||
- optional `data-shared-editable-card-collapsible`
|
||||
- optional `data-shared-editable-card-collapsed`
|
||||
- optional `data-shared-editable-card-toggle`
|
||||
- optional `data-shared-editable-card-delete`
|
||||
- optional `data-shared-editable-card-delete-confirmation`
|
||||
- optional `data-shared-editable-card-delete-input`
|
||||
- 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
|
||||
Handler.
|
||||
|
||||
## Optionales Ein- und Ausblenden
|
||||
|
||||
Eine Karte mit `data-shared-editable-card-collapsible` kann ihren Inhalt
|
||||
ein- und ausblenden. Mit `data-shared-editable-card-collapsed` wird der initial
|
||||
eingeklappte Zustand markiert. Der Toggle verwendet die Beschriftungen
|
||||
„Anzeigen“ und „Verbergen“. Beim Wechsel in den Bearbeitungsmodus wird der
|
||||
Inhalt eingeblendet und der Toggle deaktiviert. Nach dem Bearbeiten bleibt der
|
||||
aktuelle Sichtbarkeitszustand erhalten.
|
||||
|
||||
Die zentrale Komponente stellt dafür `setVisibility(card, visible)`,
|
||||
`isVisible(card)` und `setToggleEnabled(card, enabled)` bereit. Fachmodule
|
||||
verwenden diese Methoden nur für fachliche Folgeaktionen wie das erneute
|
||||
Rendern nach dem Speichern.
|
||||
|
||||
## Nutzung im Lager
|
||||
|
||||
Im Lager-Drawer verwenden die aktive Charge und offene Chargen das Pattern.
|
||||
Rein lesende Karten, insbesondere historische und virtuelle Chargen, bleiben
|
||||
beim bestehenden Content-Card-Pattern.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Mobile Detail
|
||||
|
||||
## Zweck
|
||||
|
||||
`SharedMobileDetail` ist ein fachlich neutrales Frontend-Pattern fuer mobile
|
||||
Listen-/Detailansichten. Es ersetzt auf kleinen Viewports den sichtbaren
|
||||
Drawer durch eine vollflaechige Detailansicht.
|
||||
|
||||
## Verantwortung
|
||||
|
||||
Das Pattern kapselt ausschliesslich technische Interaktion:
|
||||
|
||||
- Verschieben des vorhandenen Detail-DOMs in einen mobilen Host
|
||||
- Anzeigen und Entfernen des mobilen Hosts
|
||||
- Browser-History und `popstate`-kompatibler Zurueck-Navigation
|
||||
- Zuruecksetzen des Body-Locks und des globalen Drawer-Zustands
|
||||
- Zuruecksetzen der Drawer-Attribute beim Verlassen der Detailansicht
|
||||
|
||||
Fachliche Darstellung, API-Aufrufe, Validierung und Speichern bleiben im
|
||||
owning Modul.
|
||||
|
||||
## Nutzung
|
||||
|
||||
Fachliche Bereiche erzeugen einen Controller mit eigenem Host-/Drawer-
|
||||
Attribut, eigener Body-Klasse und einem bestehenden Styleguide-
|
||||
Navigationskarten-Markup fuer die Zurueck-Navigation. Der Controller darf
|
||||
keine fachlichen Daten oder fachlichen Zustandsentscheidungen enthalten.
|
||||
@@ -1 +1 @@
|
||||
2026.05.18.1
|
||||
2026.05.18.2
|
||||
@@ -77,8 +77,10 @@
|
||||
|
||||
<p class="sg-preview-label">Component: Large Table</p>
|
||||
<p>Suche, Sortierung und „Mehr laden“ sind hier Demo-Markup. Produktions-Implementierungen müssen Treffer serverseitig neu rendern.</p>
|
||||
<p>Optionale Spaltenbreiten-Anpassung: <code>data-column-resize="true"</code> aktiviert die Funktion. Die Tabelle benötigt dafür eine stabile, portalweit eindeutige <code>data-column-resize-key</code>. Die Header-Zellen können mit <code>data-default-width</code> (Prozent, Pixel oder relative <code>fr</code>-Werte) und die Tabelle mit <code>data-column-min-width</code> konfiguriert werden. Die Runtime wird nach dynamischem Nachladen über <code>window.sgInitLargeTableColumnResize({ root })</code> erneut aufgerufen. Gespeicherte Werte liegen als <code>{ version: 3, ratios: number[] }</code> unter <code>sg:large-table:column-widths:<key></code> in <code>localStorage</code>; die Werte sind proportionale Anteile der verfügbaren Tabellenbreite.</p>
|
||||
|
||||
<article class="sg-card sg-large-table" data-component="large-table" role="table" aria-label="Beispiel Large Table">
|
||||
<article id="styleguide-large-table" class="sg-card sg-large-table" data-component="large-table" data-column-resize="true" data-column-resize-key="styleguide-large-table" data-column-min-width="8rem" role="table" aria-label="Beispiel Large Table">
|
||||
<div class="sg-large-table__viewport" data-component-part="large-table-viewport">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header">
|
||||
<div class="sg-strong">Large Table</div>
|
||||
<span class="sg-search-field-row" data-component="search-field">
|
||||
@@ -96,31 +98,35 @@
|
||||
</div>
|
||||
|
||||
<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--header" role="row" data-component-part="large-table-header-row">
|
||||
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="0">
|
||||
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="0" data-default-width="20%">
|
||||
<button class="sg-large-table__sort-button" type="button" aria-label="Spalte 1 sortieren">
|
||||
<span class="sg-large-table__sort-label">Spalte 1</span>
|
||||
<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="ascending"></span>
|
||||
</button>
|
||||
<span class="sg-large-table__resize-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Breite von Spalte 1 anpassen" data-large-table-resize-handle="0"></span>
|
||||
</div>
|
||||
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="1">
|
||||
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="1" data-default-width="20%">
|
||||
<button class="sg-large-table__sort-button" type="button" aria-label="Spalte 2 sortieren">
|
||||
<span class="sg-large-table__sort-label">Spalte 2</span>
|
||||
<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="ascending"></span>
|
||||
</button>
|
||||
<span class="sg-large-table__resize-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Breite von Spalte 2 anpassen" data-large-table-resize-handle="1"></span>
|
||||
</div>
|
||||
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="2">
|
||||
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="2" data-default-width="20%">
|
||||
<button class="sg-large-table__sort-button" type="button" aria-label="Spalte 3 sortieren">
|
||||
<span class="sg-large-table__sort-label">Spalte 3</span>
|
||||
<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="ascending"></span>
|
||||
</button>
|
||||
<span class="sg-large-table__resize-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Breite von Spalte 3 anpassen" data-large-table-resize-handle="2"></span>
|
||||
</div>
|
||||
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="3">
|
||||
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="3" data-default-width="20%">
|
||||
<button class="sg-large-table__sort-button" type="button" aria-label="Spalte 4 sortieren">
|
||||
<span class="sg-large-table__sort-label">Spalte 4</span>
|
||||
<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="ascending"></span>
|
||||
</button>
|
||||
<span class="sg-large-table__resize-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Breite von Spalte 4 anpassen" data-large-table-resize-handle="3"></span>
|
||||
</div>
|
||||
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="4">
|
||||
<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="none" data-sort-key="4" data-default-width="20%">
|
||||
<button class="sg-large-table__sort-button" type="button" aria-label="Spalte 5 sortieren">
|
||||
<span class="sg-large-table__sort-label">Spalte 5</span>
|
||||
<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="ascending"></span>
|
||||
@@ -210,22 +216,20 @@
|
||||
|
||||
<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--load-more" role="row" data-component-part="large-table-row" data-large-table-load-more-row="true">
|
||||
<div class="sg-large-table__cell sg-large-table__cell--load-more" role="cell">
|
||||
<div class="sg-navigation-card-layout sg-large-table__load-more-layout">
|
||||
<div class="sg-navigation-card-block">
|
||||
<article class="sg-card" data-component="card" data-pattern="navigation-card" aria-label="Navigations-Card">
|
||||
<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment">
|
||||
<div class="sg-navigation-card-center">
|
||||
<a class="sg-hyperlink" href="#" data-component="hyperlink" data-large-table-load-more-trigger="true">Mehr laden</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="sg-card sg-large-table__load-more-card" data-component="card" data-pattern="navigation-card" aria-label="Navigations-Card">
|
||||
<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment">
|
||||
<div class="sg-navigation-card-center">
|
||||
<a class="sg-hyperlink" href="#" data-component="hyperlink" data-large-table-load-more-trigger="true">Mehr laden</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</section>
|
||||
|
||||
<script src="../scripts/large-table-column-resize.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,41 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Styleguide – Komponente E-Mail Footer</title>
|
||||
<link rel="stylesheet" href="../styleguide.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1 class="sg-main-heading">Komponente – E-Mail Footer</h1>
|
||||
|
||||
<section id="component-email-footer">
|
||||
<p class="sg-preview-label">Komponente: E-Mail Footer</p>
|
||||
|
||||
<p class="sg-preview-label">E-Mail Footer not subscribed</p>
|
||||
|
||||
<div class="sg-preview-area">
|
||||
<article
|
||||
class="sg-card sg-card--content-card sg-card--footer-card"
|
||||
data-component="content-card"
|
||||
data-component-context="email-footer"
|
||||
data-component-variant="not-subscribed"
|
||||
aria-label="E-Mail Footer not subscribed"
|
||||
>
|
||||
<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body">
|
||||
<div class="sg-text-layout-pattern__sample sg-text-layout-pattern__two-column" data-pattern-part="text-block-two-column">
|
||||
<p class="sg-body sg-text-layout-pattern__column">
|
||||
Footer links: Platzhaltertext fuer allgemeine Hinweise, Navigation oder Kontaktinformationen im zweispaltigen Layout.
|
||||
</p>
|
||||
<p class="sg-body sg-text-layout-pattern__column">
|
||||
Footer rechts: Platzhaltertext fuer ergaenzende Angaben, rechtliche Hinweise oder sekundäre Footer-Inhalte im gleichen Raster.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,37 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Styleguide – Komponente E-Mail Header</title>
|
||||
<link rel="stylesheet" href="../styleguide.css">
|
||||
<link rel="stylesheet" href="../email-styleguide.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1 class="sg-main-heading">Komponente – E-Mail Header</h1>
|
||||
|
||||
<section id="component-email-header">
|
||||
<p class="sg-preview-label">Komponente: E-Mail Header</p>
|
||||
|
||||
<div class="sg-preview-area">
|
||||
<div class="sg-email-preview-viewport">
|
||||
<div class="sg-email-preview-body">
|
||||
<header
|
||||
class="sg-email-header"
|
||||
aria-label="E-Mail Header"
|
||||
data-component="email-header"
|
||||
data-component-variant="logo-only"
|
||||
data-derived-from="portal-header-without-options-row"
|
||||
>
|
||||
<div class="sg-email-header__main" data-component-part="main">
|
||||
<p class="sg-email-header__brand" data-component-part="brand">ValueStockFinder</p>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -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">
|
||||
|
||||
@@ -7,6 +7,7 @@ Der Styleguide bleibt in diesem Repository die Source of Truth. Der Sync verteil
|
||||
- den deploy-relevanten CSS-Upstream versioniert als `public/assets/styleguide.upstream.css`
|
||||
- die vollstaendige Styleguide-Dokumentation gespiegelt nach `docs/styleguide`
|
||||
- die portal-spezifische Fertig-CSS-Datei als `public/assets/styles.css`
|
||||
- die konsumierbare Large-Table-Runtime als `public/scripts/large-table-column-resize.js`
|
||||
|
||||
Zielrepos:
|
||||
|
||||
@@ -31,8 +32,10 @@ Beispiel:
|
||||
|
||||
- `public/assets/styleguide.upstream.css` aktualisiert
|
||||
- `public/assets/styleguide.upstream.meta.json` aktualisiert (Version, Commit, Zeitstempel)
|
||||
- `docs/styleguide` gespiegelt (mit `--delete`, ohne `.git`, `.codex`, `AGENTS.md`, `scripts/`)
|
||||
- `docs/styleguide` gespiegelt (mit `--delete`, ohne `.git`, `.codex`, `AGENTS.md` und interne `scripts/`)
|
||||
- `public/scripts/large-table-column-resize.js` aus der Styleguide-Source-of-Truth kopiert
|
||||
- `public/assets/styles.css` aktualisiert
|
||||
- `public/scripts/large-table-column-resize.js` aktualisiert; lokale Portal-Kopien sind nicht zulässig
|
||||
- Optional: automatischer Commit + Push im Portalrepo
|
||||
|
||||
## Standardprozess je Release
|
||||
@@ -42,3 +45,18 @@ Beispiel:
|
||||
3. Styleguide commit + push
|
||||
4. Sync-Skript ausfuehren
|
||||
5. Beide Portalrepos Smoke-Testen
|
||||
|
||||
## Large-Table-Runtime
|
||||
|
||||
Die Runtime liegt ausschließlich im Styleguide unter `scripts/large-table-column-resize.js`.
|
||||
Der Sync kopiert nur diese explizit freigegebene Runtime nach `public/scripts/`; andere
|
||||
Styleguide-Scripts wie Prüf- und Sync-Shellscripts werden nicht in `docs/styleguide/scripts/`
|
||||
gespiegelt. Dadurch bleibt der Styleguide die Source of Truth und ein erneuter Sync
|
||||
überschreibt die Portaldatei kontrolliert.
|
||||
|
||||
Für den Smoke-Test in beiden Portalen ist zu prüfen:
|
||||
|
||||
- `public/scripts/large-table-column-resize.js` ist vorhanden und unter `/scripts/large-table-column-resize.js` ladbar.
|
||||
- Die Large-Table-Seite lädt die Runtime ohne lokale Inline- oder Duplikatimplementierung.
|
||||
- Nach einer Spaltenbreitenänderung wird der Eintrag unter `sg:large-table:column-widths:<key>` gespeichert.
|
||||
- Ein erneuter Sync ersetzt die Datei aus der Styleguide-Quelle.
|
||||
@@ -31,6 +31,7 @@
|
||||
<tr><td>white</td><td>#FFFFFF</td><td>Standardfläche von Buttons, Eingabefeldern, Pulldown-Triggern, Checkboxen, Radios, Toggle-Tracks und Card-Trennern.</td></tr>
|
||||
<tr><td>signal-green</td><td>#009101</td><td>Positive Score-Balken und positive Chart-Werte.</td></tr>
|
||||
<tr><td>signal-green-light</td><td>#65A665</td><td>Hellgrüne Variante für positive Signal- und Notification-Flächen.</td></tr>
|
||||
<tr><td>signal-green-red</td><td>#C0624D</td><td>Rotgrüne Variante für positive oder negative Bewertungsdarstellungen in Notifications.</td></tr>
|
||||
<tr><td>signal-yellow</td><td>#9C7A00</td><td>Neutrale Score-Balken, neutrale Chart-Werte und Warnwerte in Tabellen.</td></tr>
|
||||
<tr><td>ampel-hellgelb</td><td>#D3B962</td><td>Untersegmente in der Pattern-Notification-Variante der Object Card.</td></tr>
|
||||
<tr><td>signal-red</td><td>#9B3B2F</td><td>Negative Score-Balken und negative Chart-Werte.</td></tr>
|
||||
@@ -38,13 +39,29 @@
|
||||
<tr><td>font-light</td><td>#FFFFFF</td><td>Schrift auf dunklen Flächen, Portalheader, Card-Headern, Prozessbuttons und ausgewählten Tab-Buttons.</td></tr>
|
||||
<tr><td>font-hyperlink</td><td>#FF6900</td><td>Links in Texten, Prozessbuttons und Page-Navigations-Cards.</td></tr>
|
||||
<tr><td>process-inactive</td><td>#FFAE79</td><td>Inaktive Hintergrundfläche des Prozessbuttons.</td></tr>
|
||||
<tr><td>background-purple</td><td>#373C4A</td><td>Globaler Portal- und E-Mail-Hintergrund.</td></tr>
|
||||
<tr><td>background-purple</td><td>#373C4A</td><td>Globaler Portalhintergrund.</td></tr>
|
||||
<tr><td>background-purple-light</td><td>#656C7D</td><td>Group Cards und gruppierende Card-Flächen.</td></tr>
|
||||
<tr><td>transparent</td><td>transparent</td><td>Transparenter Foundation-Wert für semantische Overlay-/Box-Varianten ohne Eigenfläche.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="foundations-card-text">
|
||||
<h2 class="sg-sub-heading sg-text-on-dark">Card Semantic Tokens</h2>
|
||||
<table class="sg-foundation-table sg-table-label">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Wert</th>
|
||||
<th>Verwendung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>text-card-signal</td><td>font-light</td><td>Weiße Schrift auf allen farbigen Signal-Segmenten, unabhängig vom übergeordneten Card-Kontext.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="foundations-states">
|
||||
<h2 class="sg-sub-heading sg-text-on-dark">States</h2>
|
||||
<table class="sg-foundation-table sg-table-label">
|
||||
@@ -156,7 +173,6 @@
|
||||
<tr><td>input-label-width</td><td>9rem</td><td>Desktop-Breite der Label-Spalte für gruppierte Formularzeilen mit Pulldowns, Slidern, Radio-Feldern, Checkbox-Feldern sowie ein- und mehrzeiligen Eingabefeldern.</td></tr>
|
||||
<tr><td>input-field-desktop-width</td><td>400px</td><td>Fixe Desktop-Breite von ein- und mehrzeiligen Eingabefeldern in der Input-Component-Preview.</td></tr>
|
||||
<tr><td>input-field-max-width</td><td>600px</td><td>Maximale Breite von ein- und mehrzeiligen Eingabefeldern (ohne Suchfeld).</td></tr>
|
||||
<tr><td>email-body-max-width</td><td>600px</td><td>Maximale Breite des Inhaltsbereichs von E-Mail-Templates innerhalb des E-Mail-Viewports.</td></tr>
|
||||
<tr><td>object-card-height</td><td>600px</td><td>Fixe Höhe der Object Card im Desktop-Layout (u. a. für Company Card).</td></tr>
|
||||
<tr><td>scrollable-card-width</td><td>600px</td><td>Standardbreite der eigenständigen Scrollable Card; kann im konsumierenden Portal über den semantischen Layout-Token angepasst werden.</td></tr>
|
||||
<tr><td>scrollable-card-height</td><td>600px</td><td>Standardhöhe der eigenständigen Scrollable Card; kann im konsumierenden Portal über den semantischen Layout-Token angepasst werden.</td></tr>
|
||||
@@ -176,8 +192,7 @@
|
||||
<tr><td>notifications-card-min-width</td><td>445px</td><td>Mindestbreite der Notification Card im Pattern Notifications; 50px größer als die Object Card und Grundlage für Flex-Basis und Mindestbreite des Patterns.</td></tr>
|
||||
<tr><td>search-field-width</td><td>15.3rem</td><td>Fixe Breite des Suchfeld-Inputs inklusive Clear-Button-Bereich.</td></tr>
|
||||
<tr><td>layer-pulldown-panel</td><td>40</td><td>Layer-Stufe für geöffnete Pulldown-Ausklappfelder, damit sie über anderen Inhalten liegen.</td></tr>
|
||||
<tr><td>multiselect-pulldown-panel-desktop-width</td><td>500px</td><td>Reservierte Foundation-Breite für Multiselektions-Pulldown-Panels; aktuell nicht aktiv genutzt, da das Panel inhaltsbasiert mit Viewport-Kappe skaliert.</td></tr>
|
||||
<tr><td>multiselect-pulldown-panel-mobile-width</td><td>80vw</td><td>Reservierte mobile Foundation-Breite für Multiselektions-Pulldown-Panels; aktuell nicht aktiv genutzt, da das Panel inhaltsbasiert mit Viewport-Kappe skaliert.</td></tr>
|
||||
<tr><td>multiselect-pulldown-panel-desktop-width</td><td>500px</td><td>Foundation-Breite des Multiselektions-Pulldown-Panels; wird als feste Pattern-Breite mit Viewport-Kappe wiederverwendet.</td></tr>
|
||||
<tr><td>options-row-mode-toggle-width</td><td>7rem</td><td>Breite des Modus-Schiebers in der Options Row.</td></tr>
|
||||
<tr><td>options-row-help-panel-width</td><td>16rem</td><td>Breite des Help-Panels in der Options Row.</td></tr>
|
||||
<tr><td>card-list-drawer-width</td><td>40%</td><td>Relative Breite des ausziehbaren Card-Listenbereichs.</td></tr>
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<li><a href="./patterns/navigation-card.html">Navigation Card</a></li>
|
||||
<li><a href="./patterns/left-navigation.html">Left Navigation</a></li>
|
||||
<li><a href="./patterns/notifications.html">Notifications</a></li>
|
||||
<li><a href="./patterns/history-graph.html">History Graph</a></li>
|
||||
<li><a href="./patterns/content-cards-group.html">Content Cards Group</a></li>
|
||||
<li><a href="./patterns/card-gruppe-mit-tastennavigation.html">Card Gruppe mit Tastennavigation</a></li>
|
||||
<li><a href="./patterns/formular-mit-abschnitten.html">Formular mit Abschnitten</a></li>
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Styleguide – Pattern History Graph</title>
|
||||
<link rel="stylesheet" href="../styleguide.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1 class="sg-main-heading">Pattern – History Graph</h1>
|
||||
|
||||
<section id="pattern-history-graph">
|
||||
<p class="sg-preview-label">Pattern: History Graph</p>
|
||||
|
||||
<div class="sg-preview-area sg-history-graph" aria-label="Verlauf von Ereignissen">
|
||||
<ol class="sg-history-graph__events">
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Aktuellstes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green" data-component-part="card-header">
|
||||
<p class="sg-body">Aktuellstes Ereignis mit einem einzelnen farbigen Segment.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Zweites Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-yellow" data-component-part="card-header">
|
||||
<p class="sg-body">Ereignis mit mehreren unabhängig gefärbten Segmenten.</p>
|
||||
</div>
|
||||
<div class="sg-card-segment sg-card-segment--body sg-card-segment--white" data-component-part="card-body">
|
||||
<p class="sg-body">Der Inhalt, zum Beispiel Datum und Beschreibung, wird individuell durch das konsumierende Portal definiert.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Ältestes dargestelltes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-red" data-component-part="card-header">
|
||||
<p class="sg-body">Ältestes dargestelltes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Viertes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green-light" data-component-part="card-header">
|
||||
<p class="sg-body">Viertes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Fünftes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-yellow" data-component-part="card-header">
|
||||
<p class="sg-body">Fünftes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Sechstes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green" data-component-part="card-header">
|
||||
<p class="sg-body">Sechstes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Siebtes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-red" data-component-part="card-header">
|
||||
<p class="sg-body">Siebtes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Achtes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green-light" data-component-part="card-header">
|
||||
<p class="sg-body">Achtes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Neuntes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-yellow" data-component-part="card-header">
|
||||
<p class="sg-body">Neuntes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
<li class="sg-history-graph__event">
|
||||
<article class="sg-card sg-history-graph__card" data-component="card" data-pattern="history-graph-event" aria-label="Zehntes Ereignis">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green" data-component-part="card-header">
|
||||
<p class="sg-body">Zehntes Ereignis.</p>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<!-- Wird nur gerendert, wenn weitere Ereignisse geladen werden können. -->
|
||||
<div class="sg-history-graph__navigation">
|
||||
<div class="sg-navigation-card-layout">
|
||||
<div class="sg-navigation-card-block">
|
||||
<article class="sg-card" data-component="card" data-pattern="navigation-card" aria-label="Weitere Ereignisse laden">
|
||||
<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment">
|
||||
<div class="sg-navigation-card-center">
|
||||
<a class="sg-hyperlink" href="#" data-component="hyperlink">Mehr laden</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -224,14 +224,16 @@
|
||||
</div>
|
||||
|
||||
<div class="sg-pulldown-panel__row sg-pulldown-panel__row--disabled" data-pulldown-filter-row data-active="false" data-component-part="pulldown-filter-row">
|
||||
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 1: aus">
|
||||
<div class="sg-pulldown-panel__row-header">
|
||||
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 1: aus">
|
||||
<span class="sg-mode-toggle__switch" aria-hidden="true">
|
||||
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--left">aus</span>
|
||||
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--right">an</span>
|
||||
<span class="sg-mode-toggle__handle"></span>
|
||||
</span>
|
||||
</button>
|
||||
<p class="sg-pulldown-panel__label sg-body">Pulldown 1</p>
|
||||
</button>
|
||||
<p class="sg-pulldown-panel__label sg-body">Pulldown 1</p>
|
||||
</div>
|
||||
|
||||
<select class="sg-interaction-element sg-pulldown" aria-label="Pulldown 1 Auswahl" disabled>
|
||||
<option selected>Option 1</option>
|
||||
@@ -245,14 +247,16 @@
|
||||
<h2 id="multiselect-block-4" class="sg-strong sg-form-sections-card__chapter-title">Block 4</h2>
|
||||
|
||||
<div class="sg-pulldown-panel__row sg-pulldown-panel__row--disabled" data-pulldown-filter-row data-active="false" data-component-part="pulldown-filter-row">
|
||||
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 2: aus">
|
||||
<div class="sg-pulldown-panel__row-header">
|
||||
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 2: aus">
|
||||
<span class="sg-mode-toggle__switch" aria-hidden="true">
|
||||
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--left">aus</span>
|
||||
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--right">an</span>
|
||||
<span class="sg-mode-toggle__handle"></span>
|
||||
</span>
|
||||
</button>
|
||||
<p class="sg-pulldown-panel__label sg-body">Pulldown 2</p>
|
||||
</button>
|
||||
<p class="sg-pulldown-panel__label sg-body">Pulldown 2</p>
|
||||
</div>
|
||||
|
||||
<select class="sg-interaction-element sg-pulldown" aria-label="Pulldown 2 Auswahl" disabled>
|
||||
<option selected>Option 1</option>
|
||||
@@ -262,14 +266,16 @@
|
||||
</div>
|
||||
|
||||
<div class="sg-pulldown-panel__row sg-pulldown-panel__row--disabled" data-pulldown-filter-row data-active="false" data-component-part="pulldown-filter-row">
|
||||
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 3: aus">
|
||||
<div class="sg-pulldown-panel__row-header">
|
||||
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 3: aus">
|
||||
<span class="sg-mode-toggle__switch" aria-hidden="true">
|
||||
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--left">aus</span>
|
||||
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--right">an</span>
|
||||
<span class="sg-mode-toggle__handle"></span>
|
||||
</span>
|
||||
</button>
|
||||
<p class="sg-pulldown-panel__label sg-body">Pulldown 3</p>
|
||||
</button>
|
||||
<p class="sg-pulldown-panel__label sg-body">Pulldown 3</p>
|
||||
</div>
|
||||
|
||||
<select class="sg-interaction-element sg-pulldown" aria-label="Pulldown 3 Auswahl" disabled>
|
||||
<option selected>Option 1</option>
|
||||
@@ -279,14 +285,16 @@
|
||||
</div>
|
||||
|
||||
<div class="sg-pulldown-panel__row sg-pulldown-panel__row--disabled" data-pulldown-filter-row data-active="false" data-component-part="pulldown-filter-row">
|
||||
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 4: aus">
|
||||
<div class="sg-pulldown-panel__row-header">
|
||||
<button class="sg-mode-toggle sg-mode-toggle--local sg-activation-mode-toggle" type="button" data-active="absolute" aria-label="Aktivierung Pulldown 4: aus">
|
||||
<span class="sg-mode-toggle__switch" aria-hidden="true">
|
||||
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--left">aus</span>
|
||||
<span class="sg-activation-mode-toggle__switch-label sg-activation-mode-toggle__switch-label--right">an</span>
|
||||
<span class="sg-mode-toggle__handle"></span>
|
||||
</span>
|
||||
</button>
|
||||
<p class="sg-pulldown-panel__label sg-body">Pulldown 4</p>
|
||||
</button>
|
||||
<p class="sg-pulldown-panel__label sg-body">Pulldown 4</p>
|
||||
</div>
|
||||
|
||||
<select class="sg-interaction-element sg-pulldown" aria-label="Pulldown 4 Auswahl" disabled>
|
||||
<option selected>Option 1</option>
|
||||
@@ -390,28 +398,6 @@
|
||||
);
|
||||
};
|
||||
|
||||
const updateMultiselectLabelAlignment = () => {
|
||||
document.querySelectorAll('.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel').forEach((panel) => {
|
||||
const labels = panel.querySelectorAll(
|
||||
'.sg-slider-row[data-activatable="true"] > .sg-label, .sg-radio-activatable-group > .sg-label, [data-pulldown-filter-row] > .sg-pulldown-panel__label'
|
||||
);
|
||||
|
||||
let maxWidth = 0;
|
||||
labels.forEach((label) => {
|
||||
const width = Math.ceil(label.scrollWidth || label.getBoundingClientRect().width);
|
||||
if (width > maxWidth) {
|
||||
maxWidth = width;
|
||||
}
|
||||
});
|
||||
|
||||
if (maxWidth > 0) {
|
||||
panel.style.setProperty('--sg-multiselect-label-column-width', `${maxWidth}px`);
|
||||
} else {
|
||||
panel.style.removeProperty('--sg-multiselect-label-column-width');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.querySelectorAll('.sg-slider-row').forEach((row) => {
|
||||
const slider = row.querySelector('.sg-slider');
|
||||
const valueOutput = row.querySelector('.sg-slider-value');
|
||||
@@ -500,9 +486,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
updateMultiselectLabelAlignment();
|
||||
window.addEventListener('resize', updateMultiselectLabelAlignment);
|
||||
|
||||
document.querySelectorAll('.sg-pulldown-demo').forEach((demo) => {
|
||||
const trigger = demo.querySelector('.sg-pulldown-demo__trigger');
|
||||
|
||||
@@ -527,38 +510,12 @@
|
||||
}
|
||||
});
|
||||
|
||||
demo.dataset.align = 'left';
|
||||
demo.dataset.open = String(nextState);
|
||||
trigger.setAttribute('aria-expanded', String(nextState));
|
||||
|
||||
if (!nextState) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateMultiselectLabelAlignment();
|
||||
|
||||
const panel = demo.querySelector('.sg-pulldown-panel');
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
demo.dataset.align = 'left';
|
||||
panel.style.left = '0px';
|
||||
panel.style.right = 'auto';
|
||||
|
||||
const initialRect = panel.getBoundingClientRect();
|
||||
let offsetX = 0;
|
||||
const maxRight = window.innerWidth;
|
||||
|
||||
if (initialRect.right > maxRight) {
|
||||
offsetX -= initialRect.right - maxRight;
|
||||
}
|
||||
|
||||
if (initialRect.left + offsetX < 0) {
|
||||
offsetX += 0 - (initialRect.left + offsetX);
|
||||
}
|
||||
|
||||
panel.style.left = `${offsetX}px`;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -142,6 +142,27 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<p class="sg-preview-label">Pattern: Notification with title - green-red rating</p>
|
||||
<article class="sg-card sg-card--notification-signal-green-red sg-notifications-pattern__card" data-pattern="notification-with-title" data-pattern-variant="green-red-rating" data-component="notification-card">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green-red sg-notifications-pattern__text-segment sg-notifications-pattern__title-segment" data-component-part="card-header">
|
||||
<p class="sg-body">
|
||||
<span class="sg-strong">Bewertung</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green-red sg-notifications-pattern__text-segment" data-component-part="card-header">
|
||||
<p class="sg-body">
|
||||
Positive oder negative Bewertungsdarstellung zur Illustration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="sg-card-segment sg-card-segment--body sg-card-segment--signal-green-red" data-component-part="card-body">
|
||||
<button class="sg-interaction-element sg-button sg-button--active" type="button" data-component="button" data-component-state="active">
|
||||
zur Bewertung
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<p class="sg-preview-label">Pattern: Notification with title - yellow</p>
|
||||
<article class="sg-card sg-notifications-pattern__card" data-pattern="notification-with-title" data-pattern-variant="yellow" data-component="notification-card">
|
||||
<div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-yellow sg-notifications-pattern__text-segment sg-notifications-pattern__title-segment" data-component-part="card-header">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</p>
|
||||
|
||||
<p class="sg-table-label" style="color: var(--text-options-row-description);">
|
||||
Mobile: rechte Optionen werden zum oberen Segment, linke Optionen zum unteren Segment; linke Optionen brechen innerhalb der verfügbaren Breite um.
|
||||
Mobile: Pulldowns stehen mit maximal zwei Elementen pro Zeile; bei ungerader Anzahl steht der letzte Pulldown allein in einer 100-%-Zeile. Ein Suchfeld steht immer allein in einer 100-%-Zeile.
|
||||
</p>
|
||||
|
||||
<div class="sg-options-row" aria-label="Optionszeile" data-pattern="options-row">
|
||||
@@ -83,6 +83,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sg-pulldown-demo" data-open="false" data-align="left" data-selection-mode="single" data-component="pulldown" data-component-context="options-row" data-component-state="inactive-selectable">
|
||||
<button class="sg-interaction-element sg-pulldown sg-pulldown-demo__trigger" type="button" aria-expanded="false" aria-label="Very long English label for market selection" data-component-part="pulldown-trigger" data-label-base="Very long English market selection label">
|
||||
Very long English market selection label
|
||||
</button>
|
||||
|
||||
<div class="sg-pulldown-panel" aria-label="Opened market selection" data-component-part="pulldown-panel">
|
||||
<ul class="sg-pulldown-option-list" aria-label="Market options">
|
||||
<li class="sg-pulldown-option" role="checkbox" aria-checked="false" data-pulldown-option>
|
||||
<span>United Kingdom</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sg-search-field-row">
|
||||
<span class="sg-input-single-line-wrap sg-search-field-input" data-has-value="false" data-component="single-line-input" data-component-context="options-row" data-component-state="inactive-selectable">
|
||||
<input
|
||||
@@ -120,6 +134,39 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="pattern-options-row-mobile-cases">
|
||||
<p class="sg-preview-label">Pattern: Options Row – Mobile-Fälle</p>
|
||||
<p class="sg-table-label" style="color: var(--text-options-row-description);">
|
||||
Prüffälle: kein Pulldown, ein Pulldown, Suchfeld ohne Ergebniszähler sowie lange deutsche Beschriftung.
|
||||
</p>
|
||||
|
||||
<div class="sg-options-row sg-options-row--left-only" aria-label="Optionszeile ohne Pulldown" data-pattern="options-row-mobile-no-pulldown">
|
||||
<div class="sg-options-row__left" data-pattern-part="options-row-primary-actions">
|
||||
<div class="sg-search-field-row">
|
||||
<span class="sg-input-single-line-wrap sg-search-field-input" data-has-value="false" data-component="single-line-input" data-component-context="options-row" data-component-state="inactive-selectable">
|
||||
<input class="sg-interaction-element sg-input-single-line" type="text" placeholder="Suche ohne Ergebniszähler" aria-label="Suche ohne Ergebniszähler">
|
||||
<button class="sg-input-clear-button" type="button" aria-label="Eingabe löschen">×</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sg-options-row sg-options-row--left-only" aria-label="Optionszeile mit einem Pulldown" data-pattern="options-row-mobile-one-pulldown">
|
||||
<div class="sg-options-row__left" data-pattern-part="options-row-primary-actions">
|
||||
<div class="sg-pulldown-demo" data-open="false" data-align="left" data-selection-mode="single" data-component="pulldown" data-component-context="options-row" data-component-state="inactive-selectable">
|
||||
<button class="sg-interaction-element sg-pulldown sg-pulldown-demo__trigger" type="button" aria-expanded="false" aria-label="Sehr lange deutsche Beschriftung für eine Auswahl" data-component-part="pulldown-trigger" data-label-base="Sehr lange deutsche Beschriftung für eine Auswahl">
|
||||
Sehr lange deutsche Beschriftung für eine Auswahl
|
||||
</button>
|
||||
<div class="sg-pulldown-panel" aria-label="Geöffnete Auswahl" data-component-part="pulldown-panel">
|
||||
<ul class="sg-pulldown-option-list" aria-label="Auswahloptionen">
|
||||
<li class="sg-pulldown-option" role="checkbox" aria-checked="false" data-pulldown-option><span>Option</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="pattern-options-row-left-only">
|
||||
<p class="sg-preview-label">Pattern: Options row left only</p>
|
||||
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
(function initLargeTableColumnResizeModule() {
|
||||
const STORAGE_PREFIX = 'sg:large-table:column-widths:';
|
||||
const STORAGE_VERSION = 1;
|
||||
const DEFAULT_MIN_WIDTH = 128;
|
||||
const KEYBOARD_STEP = 8;
|
||||
const KEYBOARD_LARGE_STEP = 40;
|
||||
let activeResize = null;
|
||||
|
||||
const getTables = (root) => root.querySelectorAll('[data-component="large-table"][data-column-resize="true"]');
|
||||
|
||||
const getColumnHeaders = (table) => Array.from(
|
||||
table.querySelectorAll('[data-component-part="large-table-header-row"] [role="columnheader"]'),
|
||||
);
|
||||
|
||||
const getMinWidth = (table) => {
|
||||
const configured = table.dataset.columnMinWidth;
|
||||
const value = configured || getComputedStyle(table).getPropertyValue('--large-table-column-min-width').trim();
|
||||
if (!value) {
|
||||
return DEFAULT_MIN_WIDTH;
|
||||
}
|
||||
|
||||
const probe = document.createElement('span');
|
||||
probe.style.cssText = `position:absolute;width:${value};visibility:hidden;`;
|
||||
table.appendChild(probe);
|
||||
const width = probe.getBoundingClientRect().width;
|
||||
probe.remove();
|
||||
return width > 0 ? width : DEFAULT_MIN_WIDTH;
|
||||
};
|
||||
|
||||
const parseDefaultWidth = (rawValue, tableWidth) => {
|
||||
if (!rawValue) {
|
||||
return null;
|
||||
}
|
||||
const value = Number.parseFloat(rawValue);
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (rawValue.trim().endsWith('%')) {
|
||||
return tableWidth * value / 100;
|
||||
}
|
||||
if (rawValue.trim().endsWith('fr')) {
|
||||
return value;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const normalizeWidths = (widths, tableWidth, minWidth) => {
|
||||
const safeWidths = widths.map((width) => Math.max(minWidth, width));
|
||||
const total = safeWidths.reduce((sum, width) => sum + width, 0);
|
||||
if (total <= 0) {
|
||||
return Array(widths.length).fill(Math.max(minWidth, tableWidth / widths.length));
|
||||
}
|
||||
if (widths.some((width) => width !== width)) {
|
||||
return Array(widths.length).fill(Math.max(minWidth, tableWidth / widths.length));
|
||||
}
|
||||
return safeWidths.map((width) => width * tableWidth / total);
|
||||
};
|
||||
|
||||
const getDefaultWidths = (table, headers, minWidth) => {
|
||||
const tableWidth = Math.max(table.clientWidth, minWidth * headers.length);
|
||||
const configured = headers.map((header) => parseDefaultWidth(header.dataset.defaultWidth, tableWidth));
|
||||
if (configured.some((width) => width === null)) {
|
||||
return Array(headers.length).fill(Math.max(minWidth, tableWidth / headers.length));
|
||||
}
|
||||
return normalizeWidths(configured, tableWidth, minWidth);
|
||||
};
|
||||
|
||||
const getStorageKey = (table) => `${STORAGE_PREFIX}${table.dataset.columnResizeKey || ''}`;
|
||||
|
||||
const readStoredWidths = (table, count, minWidth) => {
|
||||
const key = getStorageKey(table);
|
||||
if (!table.dataset.columnResizeKey) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (!window.localStorage) {
|
||||
return null;
|
||||
}
|
||||
const stored = JSON.parse(window.localStorage.getItem(key));
|
||||
if (stored?.version !== STORAGE_VERSION || !Array.isArray(stored.widths) || stored.widths.length !== count) {
|
||||
return null;
|
||||
}
|
||||
if (stored.widths.some((width) => !Number.isFinite(width) || width < minWidth)) {
|
||||
return null;
|
||||
}
|
||||
return stored.widths;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const writeStoredWidths = (table, widths) => {
|
||||
if (!table.dataset.columnResizeKey) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!window.localStorage) {
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(getStorageKey(table), JSON.stringify({
|
||||
version: STORAGE_VERSION,
|
||||
widths: widths.map((width) => Math.round(width)),
|
||||
}));
|
||||
} catch (error) {
|
||||
// Storage can be unavailable in privacy-restricted browser contexts.
|
||||
}
|
||||
};
|
||||
|
||||
const updateHandleState = (table, widths, minWidth) => {
|
||||
const handles = table.querySelectorAll('[data-large-table-resize-handle]');
|
||||
handles.forEach((handle) => {
|
||||
const index = Number.parseInt(handle.dataset.largeTableResizeHandle, 10);
|
||||
const current = widths[index];
|
||||
const maximum = Math.max(minWidth, widths[index] + widths[index + 1] - minWidth);
|
||||
if (table.id) {
|
||||
handle.setAttribute('aria-controls', table.id);
|
||||
}
|
||||
handle.setAttribute('aria-valuemin', String(Math.round(minWidth)));
|
||||
handle.setAttribute('aria-valuemax', String(Math.round(maximum)));
|
||||
handle.setAttribute('aria-valuenow', String(Math.round(current)));
|
||||
handle.setAttribute('aria-valuetext', `${Math.round(current)} Pixel`);
|
||||
});
|
||||
};
|
||||
|
||||
const applyWidths = (table, widths, minWidth) => {
|
||||
const safeWidths = widths.map((width) => Math.max(minWidth, width));
|
||||
const total = safeWidths.reduce((sum, width) => sum + width, 0);
|
||||
table.style.setProperty('--large-table-grid-template-columns', safeWidths.map((width) => `${width}px`).join(' '));
|
||||
table.style.setProperty('--large-table-grid-min-width', `${Math.ceil(total)}px`);
|
||||
table._largeTableColumnWidths = safeWidths;
|
||||
updateHandleState(table, safeWidths, minWidth);
|
||||
};
|
||||
|
||||
const resetWidths = (table) => {
|
||||
const headers = getColumnHeaders(table);
|
||||
const minWidth = getMinWidth(table);
|
||||
const widths = getDefaultWidths(table, headers, minWidth);
|
||||
try {
|
||||
window.localStorage.removeItem(getStorageKey(table));
|
||||
} catch (error) {
|
||||
// Storage can be unavailable in privacy-restricted browser contexts.
|
||||
}
|
||||
applyWidths(table, widths, minWidth);
|
||||
};
|
||||
|
||||
const resizeColumn = (table, index, delta, persist) => {
|
||||
const widths = [...table._largeTableColumnWidths];
|
||||
const minWidth = getMinWidth(table);
|
||||
const nextLeft = Math.max(minWidth, Math.min(widths[index] + widths[index + 1] - minWidth, widths[index] + delta));
|
||||
const appliedDelta = nextLeft - widths[index];
|
||||
widths[index] = nextLeft;
|
||||
widths[index + 1] -= appliedDelta;
|
||||
applyWidths(table, widths, minWidth);
|
||||
if (persist) {
|
||||
writeStoredWidths(table, widths);
|
||||
}
|
||||
};
|
||||
|
||||
const initTable = (table) => {
|
||||
const headers = getColumnHeaders(table);
|
||||
if (headers.length < 2) {
|
||||
return;
|
||||
}
|
||||
const minWidth = getMinWidth(table);
|
||||
const stored = readStoredWidths(table, headers.length, minWidth);
|
||||
applyWidths(table, stored || getDefaultWidths(table, headers, minWidth), minWidth);
|
||||
|
||||
if (table.dataset.largeTableResizeInitialized === 'true') {
|
||||
return;
|
||||
}
|
||||
table.dataset.largeTableResizeInitialized = 'true';
|
||||
|
||||
table.addEventListener('pointerdown', (event) => {
|
||||
const handle = event.target.closest('[data-large-table-resize-handle]');
|
||||
if (!handle || !table.contains(handle)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
activeResize = {
|
||||
table,
|
||||
handle,
|
||||
index: Number.parseInt(handle.dataset.largeTableResizeHandle, 10),
|
||||
startX: event.clientX,
|
||||
startWidths: [...table._largeTableColumnWidths],
|
||||
};
|
||||
handle.dataset.largeTableResizeActive = 'true';
|
||||
table.dataset.columnResizeState = 'resizing';
|
||||
handle.setPointerCapture?.(event.pointerId);
|
||||
});
|
||||
|
||||
table.addEventListener('pointermove', (event) => {
|
||||
if (!activeResize || activeResize.table !== table) {
|
||||
return;
|
||||
}
|
||||
applyWidths(table, activeResize.startWidths, getMinWidth(table));
|
||||
resizeColumn(table, activeResize.index, event.clientX - activeResize.startX, false);
|
||||
});
|
||||
|
||||
table.addEventListener('pointerup', () => {
|
||||
if (!activeResize || activeResize.table !== table) {
|
||||
return;
|
||||
}
|
||||
writeStoredWidths(table, table._largeTableColumnWidths);
|
||||
activeResize.handle.dataset.largeTableResizeActive = 'false';
|
||||
table.dataset.columnResizeState = 'idle';
|
||||
activeResize = null;
|
||||
});
|
||||
|
||||
table.addEventListener('keydown', (event) => {
|
||||
const handle = event.target.closest('[data-large-table-resize-handle]');
|
||||
if (!handle || !table.contains(handle)) {
|
||||
return;
|
||||
}
|
||||
const index = Number.parseInt(handle.dataset.largeTableResizeHandle, 10);
|
||||
const step = event.shiftKey ? KEYBOARD_LARGE_STEP : KEYBOARD_STEP;
|
||||
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
resizeColumn(table, index, event.key === 'ArrowRight' ? step : -step, true);
|
||||
} else if (event.key === 'Home' || event.key === 'End') {
|
||||
event.preventDefault();
|
||||
const minWidth = getMinWidth(table);
|
||||
const current = table._largeTableColumnWidths[index];
|
||||
const target = event.key === 'Home' ? minWidth : current + table._largeTableColumnWidths[index + 1] - minWidth;
|
||||
resizeColumn(table, index, target - current, true);
|
||||
}
|
||||
});
|
||||
|
||||
table.addEventListener('click', (event) => {
|
||||
const reset = event.target.closest('[data-large-table-reset-widths]');
|
||||
if (reset && table.contains(reset)) {
|
||||
resetWidths(table);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.sgInitLargeTableColumnResize = (options = {}) => {
|
||||
const root = options.root || document;
|
||||
getTables(root).forEach(initTable);
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => window.sgInitLargeTableColumnResize(), { once: true });
|
||||
})();
|
||||
@@ -0,0 +1,365 @@
|
||||
(function initTooltipOverlayModule() {
|
||||
let tooltipId = 0;
|
||||
const states = new WeakMap();
|
||||
const tooltipStates = new Set();
|
||||
|
||||
const getViewport = () => ({
|
||||
width: window.visualViewport?.width || window.innerWidth,
|
||||
height: window.visualViewport?.height || window.innerHeight,
|
||||
});
|
||||
|
||||
const getPixelValue = (propertyName) => {
|
||||
const rootStyles = getComputedStyle(document.documentElement);
|
||||
const rawValue = rootStyles.getPropertyValue(propertyName).trim();
|
||||
const rootFontSize = parseFloat(rootStyles.fontSize) || 16;
|
||||
const value = parseFloat(rawValue);
|
||||
|
||||
if (Number.isNaN(value)) {
|
||||
return 0;
|
||||
}
|
||||
if (rawValue.endsWith('rem')) {
|
||||
return value * rootFontSize;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const getElements = (wrap) => ({
|
||||
trigger: wrap.querySelector('[data-component-part="tooltip-trigger"]'),
|
||||
panel: wrap.querySelector('[data-component-part="tooltip-panel"]'),
|
||||
});
|
||||
|
||||
const ensureAccessibility = (state) => {
|
||||
const { trigger, panel } = state;
|
||||
panel.setAttribute('role', 'tooltip');
|
||||
|
||||
if (!panel.id) {
|
||||
tooltipId += 1;
|
||||
panel.id = `sg-tooltip-panel-${tooltipId}`;
|
||||
}
|
||||
|
||||
const describedBy = (trigger.getAttribute('aria-describedby') || '')
|
||||
.split(/\s+/)
|
||||
.filter(Boolean);
|
||||
if (!describedBy.includes(panel.id)) {
|
||||
trigger.setAttribute('aria-describedby', [...describedBy, panel.id].join(' '));
|
||||
}
|
||||
|
||||
const hasAccessibleName = Boolean(
|
||||
trigger.getAttribute('aria-label')
|
||||
|| trigger.getAttribute('aria-labelledby')
|
||||
|| trigger.getAttribute('title')
|
||||
|| trigger.textContent.trim(),
|
||||
);
|
||||
if (!hasAccessibleName && panel.textContent.trim()) {
|
||||
trigger.setAttribute('aria-label', panel.textContent.trim());
|
||||
}
|
||||
|
||||
if (!trigger.matches('a,button,input,select,textarea,[tabindex]')) {
|
||||
trigger.setAttribute('tabindex', '0');
|
||||
}
|
||||
};
|
||||
|
||||
const setOpenState = (state, isOpen) => {
|
||||
const { wrap, panel, trigger } = state;
|
||||
wrap.dataset.tooltipOpen = String(isOpen);
|
||||
panel.hidden = !isOpen;
|
||||
|
||||
if (trigger.matches('button,[role="button"],[aria-expanded]')) {
|
||||
trigger.setAttribute('aria-expanded', String(isOpen));
|
||||
}
|
||||
};
|
||||
|
||||
const restorePanel = (state) => {
|
||||
const { panel, placeholder } = state;
|
||||
panel.hidden = true;
|
||||
panel.removeAttribute('data-tooltip-portal');
|
||||
panel.removeAttribute('data-tooltip-placement');
|
||||
panel.style.removeProperty('left');
|
||||
panel.style.removeProperty('top');
|
||||
panel.style.removeProperty('visibility');
|
||||
panel.style.removeProperty('--tooltip-arrow-offset');
|
||||
|
||||
if (placeholder.parentNode) {
|
||||
placeholder.parentNode.insertBefore(panel, placeholder.nextSibling);
|
||||
}
|
||||
};
|
||||
|
||||
const movePanelToPortal = (state) => {
|
||||
const { panel } = state;
|
||||
if (panel.parentNode !== document.body) {
|
||||
document.body.appendChild(panel);
|
||||
}
|
||||
panel.setAttribute('data-tooltip-portal', 'true');
|
||||
panel.hidden = false;
|
||||
panel.style.visibility = 'hidden';
|
||||
};
|
||||
|
||||
const setArrowOffset = (state, placement, panelLeft, panelTop, panelWidth, panelHeight) => {
|
||||
const { trigger, panel } = state;
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const offset = placement === 'left' || placement === 'right'
|
||||
? triggerRect.top + (triggerRect.height / 2) - panelTop
|
||||
: triggerRect.left + (triggerRect.width / 2) - panelLeft;
|
||||
const maxOffset = placement === 'left' || placement === 'right' ? panelHeight : panelWidth;
|
||||
panel.style.setProperty('--tooltip-arrow-offset', `${Math.max(0, Math.min(offset, maxOffset))}px`);
|
||||
};
|
||||
|
||||
const positionTooltip = (state) => {
|
||||
const { trigger, panel } = state;
|
||||
if (panel.hidden || panel.parentNode !== document.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewport = getViewport();
|
||||
const safeInset = getPixelValue('--spacing-large');
|
||||
const gap = getPixelValue('--spacing-small');
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const panelWidth = panel.offsetWidth;
|
||||
const panelHeight = panel.offsetHeight;
|
||||
const centeredLeft = triggerRect.left + (triggerRect.width / 2) - (panelWidth / 2);
|
||||
const centeredRight = centeredLeft + panelWidth;
|
||||
const hasBottomSpace = triggerRect.bottom + gap + panelHeight <= viewport.height - safeInset;
|
||||
const hasTopSpace = triggerRect.top - gap - panelHeight >= safeInset;
|
||||
const hasLeftSpace = triggerRect.left - gap - panelWidth >= safeInset;
|
||||
const hasRightSpace = triggerRect.right + gap + panelWidth <= viewport.width - safeInset;
|
||||
|
||||
let placement = 'bottom';
|
||||
if (centeredRight > viewport.width - safeInset && hasLeftSpace) {
|
||||
placement = 'left';
|
||||
} else if (centeredLeft < safeInset && hasRightSpace) {
|
||||
placement = 'right';
|
||||
} else if (!hasBottomSpace && hasTopSpace) {
|
||||
placement = 'top';
|
||||
}
|
||||
|
||||
let panelLeft = centeredLeft;
|
||||
let panelTop = triggerRect.bottom + gap;
|
||||
|
||||
if (placement === 'top') {
|
||||
panelTop = triggerRect.top - gap - panelHeight;
|
||||
} else if (placement === 'left') {
|
||||
panelLeft = triggerRect.left - gap - panelWidth;
|
||||
panelTop = triggerRect.top + (triggerRect.height / 2) - (panelHeight / 2);
|
||||
} else if (placement === 'right') {
|
||||
panelLeft = triggerRect.right + gap;
|
||||
panelTop = triggerRect.top + (triggerRect.height / 2) - (panelHeight / 2);
|
||||
}
|
||||
|
||||
panelLeft = Math.max(safeInset, Math.min(panelLeft, viewport.width - safeInset - panelWidth));
|
||||
panelTop = Math.max(safeInset, Math.min(panelTop, viewport.height - safeInset - panelHeight));
|
||||
|
||||
panel.dataset.tooltipPlacement = placement;
|
||||
state.wrap.dataset.tooltipPlacement = placement;
|
||||
panel.style.left = `${Math.round(panelLeft)}px`;
|
||||
panel.style.top = `${Math.round(panelTop)}px`;
|
||||
setArrowOffset(state, placement, panelLeft, panelTop, panelWidth, panelHeight);
|
||||
panel.style.visibility = 'visible';
|
||||
};
|
||||
|
||||
const openTooltip = (state) => {
|
||||
tooltipStates.forEach((otherState) => {
|
||||
if (otherState !== state) {
|
||||
closeTooltip(otherState);
|
||||
}
|
||||
});
|
||||
movePanelToPortal(state);
|
||||
setOpenState(state, true);
|
||||
positionTooltip(state);
|
||||
};
|
||||
|
||||
const closeTooltip = (state) => {
|
||||
state.pinned = false;
|
||||
state.hoverTrigger = false;
|
||||
state.hoverPanel = false;
|
||||
state.focused = false;
|
||||
state.dismissed = false;
|
||||
setOpenState(state, false);
|
||||
restorePanel(state);
|
||||
};
|
||||
|
||||
const closeIfInactive = (state) => {
|
||||
if (!state.pinned && !state.hoverTrigger && !state.hoverPanel && !state.focused) {
|
||||
setOpenState(state, false);
|
||||
restorePanel(state);
|
||||
}
|
||||
};
|
||||
|
||||
const togglePinned = (state) => {
|
||||
if (state.pinned) {
|
||||
state.pinned = false;
|
||||
state.dismissed = true;
|
||||
setOpenState(state, false);
|
||||
restorePanel(state);
|
||||
return;
|
||||
}
|
||||
|
||||
state.pinned = true;
|
||||
state.dismissed = false;
|
||||
openTooltip(state);
|
||||
};
|
||||
|
||||
const initTooltip = (wrap) => {
|
||||
if (states.has(wrap)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = getElements(wrap);
|
||||
if (!elements.trigger || !elements.panel || !elements.panel.parentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const placeholder = document.createComment('sg-tooltip-panel-placeholder');
|
||||
elements.panel.parentNode.insertBefore(placeholder, elements.panel);
|
||||
|
||||
const state = {
|
||||
wrap,
|
||||
trigger: elements.trigger,
|
||||
panel: elements.panel,
|
||||
placeholder,
|
||||
pinned: false,
|
||||
hoverTrigger: false,
|
||||
hoverPanel: false,
|
||||
focused: false,
|
||||
dismissed: false,
|
||||
};
|
||||
wrap.dataset.tooltipInit = 'true';
|
||||
wrap.dataset.tooltipPlacement = 'bottom';
|
||||
states.set(wrap, state);
|
||||
tooltipStates.add(state);
|
||||
ensureAccessibility(state);
|
||||
setOpenState(state, false);
|
||||
|
||||
wrap.addEventListener('pointerenter', (event) => {
|
||||
if (event.pointerType !== 'mouse') {
|
||||
return;
|
||||
}
|
||||
if (state.dismissed) {
|
||||
return;
|
||||
}
|
||||
state.hoverTrigger = true;
|
||||
openTooltip(state);
|
||||
});
|
||||
|
||||
wrap.addEventListener('pointerleave', (event) => {
|
||||
if (event.pointerType !== 'mouse') {
|
||||
return;
|
||||
}
|
||||
state.hoverTrigger = false;
|
||||
state.dismissed = false;
|
||||
if (!state.panel.contains(event.relatedTarget)) {
|
||||
closeIfInactive(state);
|
||||
}
|
||||
});
|
||||
|
||||
state.panel.addEventListener('pointerenter', (event) => {
|
||||
if (event.pointerType !== 'mouse') {
|
||||
return;
|
||||
}
|
||||
if (!state.dismissed) {
|
||||
state.hoverPanel = true;
|
||||
openTooltip(state);
|
||||
}
|
||||
});
|
||||
|
||||
state.panel.addEventListener('pointerleave', (event) => {
|
||||
if (event.pointerType !== 'mouse') {
|
||||
return;
|
||||
}
|
||||
state.hoverPanel = false;
|
||||
state.dismissed = false;
|
||||
closeIfInactive(state);
|
||||
});
|
||||
|
||||
wrap.addEventListener('focusin', () => {
|
||||
if (!state.dismissed) {
|
||||
state.focused = true;
|
||||
openTooltip(state);
|
||||
}
|
||||
});
|
||||
|
||||
wrap.addEventListener('focusout', (event) => {
|
||||
if (wrap.contains(event.relatedTarget) || state.panel.contains(event.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
state.focused = false;
|
||||
state.dismissed = false;
|
||||
closeIfInactive(state);
|
||||
});
|
||||
|
||||
state.trigger.addEventListener('click', (event) => {
|
||||
togglePinned(state);
|
||||
});
|
||||
|
||||
state.trigger.addEventListener('keydown', (event) => {
|
||||
const isNativeInteractive = state.trigger.matches('a,button,input,select,textarea');
|
||||
if (!isNativeInteractive && (event.key === 'Enter' || event.key === ' ')) {
|
||||
event.preventDefault();
|
||||
togglePinned(state);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const initializeWithin = (root) => {
|
||||
if (root.nodeType === Node.DOCUMENT_NODE) {
|
||||
root.querySelectorAll('.sg-tooltip-wrap').forEach(initTooltip);
|
||||
return;
|
||||
}
|
||||
if (root.nodeType !== Node.ELEMENT_NODE) {
|
||||
return;
|
||||
}
|
||||
const owningWrap = root.closest('.sg-tooltip-wrap');
|
||||
if (owningWrap) {
|
||||
initTooltip(owningWrap);
|
||||
}
|
||||
if (root.matches('.sg-tooltip-wrap')) {
|
||||
initTooltip(root);
|
||||
}
|
||||
root.querySelectorAll('.sg-tooltip-wrap').forEach(initTooltip);
|
||||
};
|
||||
|
||||
const bootstrap = () => {
|
||||
window.sgInitTooltips = (options = {}) => {
|
||||
const root = options.root || document.body;
|
||||
initializeWithin(root);
|
||||
};
|
||||
|
||||
window.sgInitTooltips();
|
||||
|
||||
const observer = new MutationObserver((records) => {
|
||||
records.forEach((record) => {
|
||||
record.addedNodes.forEach(initializeWithin);
|
||||
});
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
const closeFromOutside = (event) => {
|
||||
tooltipStates.forEach((state) => {
|
||||
if (!state.wrap.contains(event.target) && !state.panel.contains(event.target)) {
|
||||
closeTooltip(state);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('pointerdown', closeFromOutside, true);
|
||||
document.addEventListener('touchstart', closeFromOutside, true);
|
||||
document.addEventListener('click', closeFromOutside, true);
|
||||
|
||||
const repositionOpenTooltips = () => {
|
||||
document.querySelectorAll('.sg-tooltip-wrap[data-tooltip-open="true"]').forEach((wrap) => {
|
||||
const state = states.get(wrap);
|
||||
if (state) {
|
||||
positionTooltip(state);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', repositionOpenTooltips);
|
||||
window.addEventListener('scroll', repositionOpenTooltips, true);
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
|
||||
} else {
|
||||
bootstrap();
|
||||
}
|
||||
})();
|
||||
@@ -199,7 +199,6 @@
|
||||
<tr><td>layout-pulldown-padding-inline</td><td>compact-interaction-padding-horizontal</td><td>Innenabstand links für Pulldown-Trigger und Select-Felder.</td></tr>
|
||||
<tr><td>layout-pulldown-chevron-offset</td><td>compact-interaction-padding-horizontal</td><td>Rechter Innenabstand des Chevron-Icons im Pulldown.</td></tr>
|
||||
<tr><td>layout-pulldown-chevron-reserved-space</td><td>spacing-large</td><td>Reservierte Breite zwischen Pulldown-Text und rechtem Chevron-Bereich.</td></tr>
|
||||
<tr><td>layout-pulldown-panel-form-mobile-width</td><td>90%</td><td>Zielbreite für Pulldown-Panels mit Formularinhalt auf Mobile; wird durch die Viewport-Kappe begrenzt.</td></tr>
|
||||
<tr><td>layer-pulldown-panel</td><td>dimension-layer-pulldown-panel</td><td>Layer-Stufe geöffneter Pulldown-Ausklappfelder über übrigen Seiteninhalten.</td></tr>
|
||||
<tr><td>layout-multiselect-pulldown-panel-desktop-width</td><td>dimension-multiselect-pulldown-panel-desktop-width</td><td>Semantische Obergrenze für die Panelbreite geöffneter Pulldowns; das Panel darf über Triggerbreite wachsen, bleibt aber durch diese Breite und die Viewport-Kappe begrenzt.</td></tr>
|
||||
<tr><td>surface-menu-panel-portal</td><td>color-light-grey</td><td>Fläche des Sandwich-Menü-Panels für große und kleine Variante.</td></tr>
|
||||
@@ -224,6 +223,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-white</td><td>Fläche der generischen Tooltip-Sprechblase.</td></tr>
|
||||
<tr><td>text-tooltip</td><td>color-font-dark</td><td>Textfarbe der generischen Tooltip-Sprechblase.</td></tr>
|
||||
<tr><td>layout-tooltip-width</td><td>calc(interaction-height * 8)</td><td>Standardbreite der generischen Tooltip-Sprechblase.</td></tr>
|
||||
<tr><td>radius-tooltip</td><td>radius-interaction</td><td>Radius der generischen Tooltip-Sprechblase.</td></tr>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -129,7 +129,16 @@
|
||||
|
||||
<h3 class="sg-sub-heading sg-section-h3">Multiselektions-Pulldown</h3>
|
||||
<table class="sg-foundation-table sg-table-label"><thead><tr><th>Semantischer Token</th><th>Verwendeter Foundation-Token</th><th>Beschreibung Verwendungszweck im Pattern</th></tr></thead><tbody>
|
||||
<tr><td>layout-multiselect-pulldown-label-column-width-fallback</td><td>max-content</td><td>Fallback-Breite der gemeinsamen Labelspalte im Panel, bis die längste Labelbreite per Pattern-Logik ermittelt wurde.</td></tr>
|
||||
<tr><td>layout-multiselect-panel-width</td><td>layout-multiselect-pulldown-panel-desktop-width</td><td>Feste Breite des Multiselektions-Pulldown-Panels auf allen Viewports innerhalb der vorhandenen Viewport-Kappe.</td></tr>
|
||||
<tr><td>layout-multiselect-panel-available-width</td><td>Viewportbreite minus 2 × spacing-large</td><td>Viewport-Kappe für das Multiselektions-Pulldown-Panel mit gleichmäßigem Außenabstand.</td></tr>
|
||||
<tr><td>layout-multiselect-chapter-gap</td><td>spacing-large</td><td>Einheitlicher vertikaler Abstand zwischen allen direkten Elementen eines Multiselect-Blocks.</td></tr>
|
||||
<tr><td>layout-multiselect-column-gap</td><td>spacing-large</td><td>Horizontaler Abstand zwischen Aktivierungsschalter und Label in den Pattern-Zeilen.</td></tr>
|
||||
<tr><td>layout-multiselect-control-gap</td><td>spacing-small</td><td>Standardabstand innerhalb der Slider-Zeilen.</td></tr>
|
||||
<tr><td>layout-multiselect-visual-control-gap</td><td>2 × spacing-small</td><td>Visueller Abstand zwischen Kopfzeile und Interaktionselement bei Radio- und Pulldown-Zeilen.</td></tr>
|
||||
<tr><td>layout-multiselect-control-row-height</td><td>interaction-height</td><td>Höhe der Kopfzeile in den filterbaren Pulldown-Zeilen.</td></tr>
|
||||
<tr><td>layout-multiselect-value-min-width</td><td>1.25 × interaction-height</td><td>Mindestbreite der Slider-Wertanzeige.</td></tr>
|
||||
<tr><td>typography-multiselect-label-line-height</td><td>line-height-base</td><td>Zeilenhöhe der Labels in den filterbaren Pulldown-Zeilen.</td></tr>
|
||||
<tr><td>opacity-multiselect-disabled</td><td>disabled-opacity</td><td>Deckkraft deaktivierter Multiselect-Inhalte.</td></tr>
|
||||
</tbody></table>
|
||||
|
||||
<h3 class="sg-sub-heading sg-section-h3">Overlay Card</h3>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@import "./styles/03-typography-helpers.css";
|
||||
@import "./styles/10-components-interactive-elements.css";
|
||||
@import "./styles/47-patterns-file-upload.css";
|
||||
@import "./styles/48-patterns-history-graph.css";
|
||||
@import "./styles/20-patterns-portal-header.css";
|
||||
@import "./styles/21-patterns-options-row.css";
|
||||
@import "./styles/22-patterns-object-card.css";
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
--color-process-inactive: #FFAE79;
|
||||
--color-signal-green: #009101;
|
||||
--color-signal-green-light: #65A665;
|
||||
--color-signal-green-red: #C0624D;
|
||||
--color-signal-yellow: #9C7A00;
|
||||
--color-ampel-hellgelb: #D3B962;
|
||||
--color-signal-red: #9B3B2F;
|
||||
@@ -37,6 +38,7 @@
|
||||
--surface-card-header-muted: var(--color-darkbrown);
|
||||
--divider-card-segment: var(--color-white);
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-signal: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-dark);
|
||||
--text-card-transparent: var(--color-font-light);
|
||||
--layout-card-body-content-justify: flex-start;
|
||||
@@ -62,6 +64,7 @@
|
||||
--surface-menu-panel-portal: var(--color-light-grey);
|
||||
--surface-help-icon: var(--color-medium-grey);
|
||||
--surface-help-panel: var(--color-light-grey);
|
||||
--surface-tooltip: var(--color-white);
|
||||
--surface-pulldown-panel: var(--color-light-grey);
|
||||
--surface-activatable-remove: var(--surface-control-default);
|
||||
--surface-slider-track: var(--color-medium-grey);
|
||||
@@ -76,10 +79,8 @@
|
||||
--layout-pulldown-chevron-offset: var(--compact-interaction-padding-horizontal);
|
||||
--layout-tab-navigation-large-padding-inline: var(--interaction-padding-horizontal);
|
||||
--layout-pulldown-chevron-reserved-space: var(--spacing-large);
|
||||
--layout-pulldown-panel-form-mobile-width: 90%;
|
||||
--layer-pulldown-panel: var(--dimension-layer-pulldown-panel);
|
||||
--layout-multiselect-pulldown-panel-desktop-width: var(--dimension-multiselect-pulldown-panel-desktop-width);
|
||||
--layout-multiselect-pulldown-label-column-width-fallback: max-content;
|
||||
--layout-input-label-width: var(--dimension-input-label-width);
|
||||
--layout-input-field-desktop-width: var(--dimension-input-field-desktop-width);
|
||||
--layout-input-field-max-width: var(--dimension-input-field-max-width);
|
||||
@@ -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");
|
||||
@@ -140,7 +144,6 @@
|
||||
--layout-preview-align-items: flex-start;
|
||||
|
||||
/* Semantic pattern tokens: Portal header */
|
||||
--surface-portal-background: var(--color-background-purple);
|
||||
--surface-portal-header: var(--color-darkblue);
|
||||
--surface-portal-header-tab: var(--color-midblue);
|
||||
--surface-portal-header-tab-active: var(--color-light-grey);
|
||||
@@ -306,7 +309,6 @@
|
||||
--dimension-input-label-width: 9rem;
|
||||
--dimension-input-field-desktop-width: 400px;
|
||||
--dimension-input-field-max-width: 600px;
|
||||
--dimension-email-body-max-width: 600px;
|
||||
--dimension-search-field-width: 15.3rem;
|
||||
--dimension-public-content-width: 100%;
|
||||
--dimension-layer-pulldown-panel: 40;
|
||||
|
||||
@@ -8,7 +8,7 @@ body {
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-base);
|
||||
color: var(--color-font-dark);
|
||||
background: var(--surface-portal-background);
|
||||
background: var(--color-background-purple);
|
||||
margin: 0;
|
||||
padding: var(--spacing-small);
|
||||
}
|
||||
|
||||
@@ -1239,6 +1239,134 @@
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel {
|
||||
position: absolute;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
display: block;
|
||||
width: var(--layout-tooltip-width);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: anywhere;
|
||||
padding: var(--spacing-large);
|
||||
border-radius: var(--radius-tooltip);
|
||||
color: var(--text-tooltip);
|
||||
background: var(--surface-tooltip);
|
||||
box-shadow: var(--shadow-overlay);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel {
|
||||
top: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel {
|
||||
bottom: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
right: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
left: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel::before {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel::before {
|
||||
top: calc(var(--spacing-small) * -2);
|
||||
left: 50%;
|
||||
border-width: 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
|
||||
border-color: transparent transparent var(--surface-tooltip);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel::before {
|
||||
bottom: calc(var(--spacing-small) * -2);
|
||||
left: 50%;
|
||||
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
|
||||
border-color: var(--surface-tooltip) transparent transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel::before {
|
||||
top: 50%;
|
||||
right: calc(var(--spacing-small) * -2);
|
||||
border-width: calc(var(--spacing-small) * 2) 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
|
||||
border-color: transparent transparent transparent var(--surface-tooltip);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel::before {
|
||||
top: 50%;
|
||||
left: calc(var(--spacing-small) * -2);
|
||||
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"] {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="bottom"]::before {
|
||||
top: calc(var(--spacing-small) * -2);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
|
||||
border-color: transparent transparent var(--surface-tooltip);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="top"]::before {
|
||||
bottom: calc(var(--spacing-small) * -2);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
|
||||
border-color: var(--surface-tooltip) transparent transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="left"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
right: calc(var(--spacing-small) * -2);
|
||||
border-width: calc(var(--spacing-small) * 2) 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
|
||||
border-color: transparent transparent transparent var(--surface-tooltip);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="right"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
left: calc(var(--spacing-small) * -2);
|
||||
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-pulldown-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + var(--spacing-small));
|
||||
|
||||
@@ -91,19 +91,44 @@
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left {
|
||||
order: 0;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-pulldown-demo,
|
||||
.sg-options-row__left > .sg-input-single-line-wrap,
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 1 1 calc(50% - var(--layout-options-row-group-gap));
|
||||
.sg-options-row__left > .sg-pulldown-demo {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left:has(> .sg-pulldown-demo ~ .sg-pulldown-demo) > .sg-pulldown-demo {
|
||||
flex-basis: calc(50% - (var(--layout-options-row-group-gap) / 2));
|
||||
}
|
||||
|
||||
.sg-options-row:not(.sg-options-row--left-only) > .sg-options-row__left:has(> .sg-pulldown-demo:nth-last-child(2):nth-child(odd)) > .sg-pulldown-demo:nth-last-child(2),
|
||||
.sg-options-row:not(.sg-options-row--left-only) > .sg-options-row__left:has(> .sg-pulldown-demo:last-child:nth-child(odd)) > .sg-pulldown-demo:last-child {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left > .sg-pulldown-demo {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 0 0 auto;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row > .sg-search-field-input {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row .sg-input-single-line {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-input-single-line-wrap .sg-input-single-line,
|
||||
@@ -111,15 +136,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
|
||||
width: min(
|
||||
var(--layout-pulldown-panel-form-mobile-width),
|
||||
calc(100vw - (2 * var(--spacing-large)))
|
||||
);
|
||||
min-width: 0;
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
}
|
||||
|
||||
.sg-options-row .sg-pulldown-panel .sg-form-sections-card-wrapper,
|
||||
.sg-options-row .sg-pulldown-panel .sg-form-sections-card {
|
||||
min-width: 0;
|
||||
|
||||
@@ -75,39 +75,63 @@
|
||||
padding-left: var(--layout-form-sections-interaction-indent);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > :not(.sg-form-sections-card__chapter-title) + :not(.sg-form-sections-card__chapter-title) {
|
||||
margin-top: var(--spacing-large);
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] {
|
||||
--layout-multiselect-panel-width: var(--layout-multiselect-pulldown-panel-desktop-width);
|
||||
--layout-multiselect-panel-available-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
--layout-multiselect-chapter-gap: var(--spacing-large);
|
||||
--layout-multiselect-column-gap: var(--spacing-large);
|
||||
--layout-multiselect-control-gap: var(--spacing-small);
|
||||
--layout-multiselect-visual-control-gap: calc(var(--spacing-small) * 2);
|
||||
--layout-multiselect-control-row-height: var(--interaction-height);
|
||||
--layout-multiselect-value-min-width: calc(var(--interaction-height) * 1.25);
|
||||
--typography-multiselect-label-line-height: var(--line-height-base);
|
||||
--opacity-multiselect-disabled: var(--disabled-opacity);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-form-sections-card__chapter-title + * {
|
||||
margin-top: var(--spacing-large);
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > * + * {
|
||||
margin-top: var(--layout-multiselect-chapter-gap);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-slider-row + .sg-slider-row,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-checkbox-field-option + .sg-checkbox-field-option,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-radio-activatable-group + .sg-radio-activatable-group {
|
||||
margin-top: var(--spacing-small);
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-options-row__left > .sg-pulldown-demo {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-pulldown-panel__row + .sg-pulldown-panel__row {
|
||||
margin-top: var(--spacing-large);
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-options-row__left > .sg-pulldown-demo .sg-pulldown {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
|
||||
--sg-multiselect-label-column-width: var(--layout-multiselect-pulldown-label-column-width-fallback);
|
||||
width: min(
|
||||
var(--layout-multiselect-panel-width),
|
||||
var(--layout-multiselect-panel-available-width)
|
||||
);
|
||||
min-width: min(
|
||||
var(--layout-multiselect-panel-width),
|
||||
var(--layout-multiselect-panel-available-width)
|
||||
);
|
||||
max-width: var(--layout-multiselect-panel-available-width);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel .sg-form-sections-card-wrapper,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel .sg-form-sections-card {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] > .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
|
||||
white-space: nowrap;
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] .sg-pulldown-panel__label {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
|
||||
display: grid;
|
||||
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr) auto;
|
||||
grid-template-columns: max-content minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
column-gap: var(--spacing-large);
|
||||
column-gap: var(--layout-multiselect-column-gap);
|
||||
row-gap: var(--layout-multiselect-control-gap);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable {
|
||||
@@ -115,37 +139,43 @@
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-slider {
|
||||
opacity: var(--disabled-opacity);
|
||||
opacity: var(--opacity-multiselect-disabled);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-slider-value,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-radio-activatable-group__choices {
|
||||
opacity: var(--disabled-opacity);
|
||||
opacity: var(--opacity-multiselect-disabled);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-mode-toggle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider {
|
||||
grid-column: 3;
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 2;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider-value {
|
||||
grid-column: 4;
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
min-width: var(--layout-multiselect-value-min-width);
|
||||
line-height: 1;
|
||||
align-self: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
|
||||
@@ -154,22 +184,26 @@
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group {
|
||||
display: grid;
|
||||
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr);
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
align-items: center;
|
||||
column-gap: var(--spacing-large);
|
||||
column-gap: var(--layout-multiselect-column-gap);
|
||||
row-gap: var(--layout-multiselect-visual-control-gap);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-mode-toggle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-radio-activatable-group__choices {
|
||||
grid-column: 3;
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-radio-activatable-group__choices {
|
||||
@@ -178,119 +212,45 @@
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable > :not(.sg-mode-toggle),
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > :not(.sg-mode-toggle),
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > :not(.sg-mode-toggle) {
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__row-header > :not(.sg-mode-toggle),
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] {
|
||||
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr);
|
||||
column-gap: var(--spacing-large);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: var(--layout-multiselect-control-row-height) auto;
|
||||
row-gap: var(--layout-multiselect-visual-control-gap);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-mode-toggle {
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__row-header {
|
||||
display: flex;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
align-items: center;
|
||||
gap: var(--layout-multiselect-column-gap);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
|
||||
grid-column: 2;
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__row-header > .sg-pulldown-panel__label {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
line-height: var(--typography-multiselect-label-line-height);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown {
|
||||
grid-column: 3;
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__row-header > .sg-pulldown-panel__label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown {
|
||||
opacity: var(--disabled-opacity);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-demo .sg-pulldown-panel {
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
|
||||
grid-template-columns: max-content minmax(0, 1fr) auto;
|
||||
row-gap: var(--spacing-small);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] > .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-mode-toggle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider-value {
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group {
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
row-gap: var(--spacing-small);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-mode-toggle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-radio-activatable-group__choices {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] {
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
row-gap: var(--spacing-small);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-mode-toggle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
}
|
||||
opacity: var(--opacity-multiselect-disabled);
|
||||
}
|
||||
|
||||
.sg-form-sections-card__actions {
|
||||
|
||||
@@ -136,22 +136,27 @@
|
||||
|
||||
.sg-card-segment--signal-red {
|
||||
background: var(--color-signal-red);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green {
|
||||
background: var(--color-signal-green);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-light {
|
||||
background: var(--color-signal-green-light);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-red {
|
||||
background: var(--color-signal-green-red);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-yellow {
|
||||
background: var(--color-signal-yellow);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--gray {
|
||||
@@ -171,6 +176,11 @@
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card.sg-card--notification-signal-green-red {
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card--content-card {
|
||||
--surface-card-header-primary: var(--surface-content-block-card-title);
|
||||
--surface-card-body: var(--surface-content-block-card-content);
|
||||
|
||||
@@ -71,16 +71,32 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-large-table[data-column-resize="true"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sg-large-table__viewport {
|
||||
align-self: stretch;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sg-large-table__title-segment {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-large);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sg-large-table__row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
grid-template-columns: var(--large-table-grid-template-columns, repeat(5, minmax(0, 1fr)));
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
@@ -88,6 +104,11 @@
|
||||
background: var(--color-white);
|
||||
}
|
||||
|
||||
.sg-large-table[data-column-resize="true"] .sg-large-table__row,
|
||||
.sg-large-table[data-column-resize="true"] .sg-large-table__title-segment {
|
||||
width: max(100%, var(--large-table-grid-min-width, 0px));
|
||||
}
|
||||
|
||||
.sg-large-table__row--hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -119,16 +140,59 @@
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.sg-large-table__load-more-layout {
|
||||
.sg-large-table__row--load-more,
|
||||
.sg-large-table__cell--load-more,
|
||||
.sg-large-table__load-more-card {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sg-large-table[data-column-resize="true"] .sg-large-table__row--load-more {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.sg-large-table__cell--header {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sg-large-table__resize-handle {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
right: -0.25rem;
|
||||
bottom: 0;
|
||||
width: 0.5rem;
|
||||
cursor: col-resize;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.sg-large-table__resize-handle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
bottom: 25%;
|
||||
left: 50%;
|
||||
width: 0.1rem;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-darkblue);
|
||||
box-shadow: -0.15rem 0 0 var(--color-darkblue), 0.15rem 0 0 var(--color-darkblue);
|
||||
}
|
||||
|
||||
.sg-large-table__resize-handle:hover::after,
|
||||
.sg-large-table__resize-handle:focus-visible::after,
|
||||
.sg-large-table[data-column-resize-state="resizing"] .sg-large-table__resize-handle[data-large-table-resize-active="true"]::after {
|
||||
background: var(--color-darkblue);
|
||||
box-shadow: -0.2rem 0 0 var(--color-darkblue), 0.2rem 0 0 var(--color-darkblue);
|
||||
}
|
||||
|
||||
.sg-large-table__resize-handle:focus-visible {
|
||||
outline: 2px solid var(--color-darkblue);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.sg-large-table__sort-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/* ========================================================= */
|
||||
/* Pattern: History Graph */
|
||||
/* ========================================================= */
|
||||
|
||||
.sg-history-graph {
|
||||
--layout-history-graph-connector-width: var(--score-marker-width);
|
||||
--surface-history-graph-connector: var(--chart-marker-line);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sg-history-graph__event {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__event:not(:last-child)::after {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
width: var(--layout-history-graph-connector-width);
|
||||
height: var(--spacing-large);
|
||||
background: var(--surface-history-graph-connector);
|
||||
content: "";
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-history-graph__card {
|
||||
width: 100%;
|
||||
max-width: var(--layout-object-card-min-width);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.sg-history-graph__navigation {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
# Technische Architektur
|
||||
|
||||
Stand: 2026-04-08
|
||||
Status: Verbindlicher Architektur-Master
|
||||
Dokumentklasse: normative
|
||||
|
||||
## 1. Zweck und Geltung
|
||||
|
||||
Dieses Dokument ist die verbindliche Architekturvorgabe fuer die Entwicklung hochmodularer Webportale in diesem Repository. Es steuert Analyse, Implementierung, Refactoring und Review durch Mensch und LLM.
|
||||
|
||||
Verbindliche Betriebsregel:
|
||||
|
||||
- Entwicklung und erste Integration erfolgen lokal auf `feature/<thema>` im lokalen Docker-Stack.
|
||||
- `main` ist der gemeinsame, auf DEV betriebene Integrationsstand. Ein Feature wird erst nach lokaler Verifikation und expliziter Freigabe nach `main` gemergt.
|
||||
- PROD wird ausschliesslich durch das bestehende Release-Skript mit einem expliziten Release-Tag eines in DEV getesteten `main`-Commits ausgerollt und nur nach expliziter User-Freigabe angefasst.
|
||||
|
||||
Es definiert verbindlich:
|
||||
|
||||
- Modul-, Prozess-, Frontend-, API- und Datenbankgrenzen
|
||||
- Ownership und oeffentliche Schnittstellen
|
||||
- Standard-Scope fuer Analyse und Implementierung
|
||||
- Wiederverwendungs- und Redundanzregeln
|
||||
- Regeln fuer neue Module, Submodule und zentrale Bausteine
|
||||
|
||||
Dieses Dokument ist kein Inventar konkreter Tabellen, Prozesse, Dateien oder Runtime-Artefakte. Solche Details liegen in den zustaendigen Modul-, Prozess- oder DB-Vertraegen.
|
||||
|
||||
## 2. Architekturprinzipien
|
||||
|
||||
### 2.1 Module
|
||||
|
||||
Ein Modul ist ein fachlich oder technisch gekapselter Contract, nicht nur eine Verzeichnisstruktur.
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- jedes Modul besitzt genau eine klar abgegrenzte Primaerverantwortung
|
||||
- jedes Modul kapselt seine Domain-Logik, Datenzugriffe, externen Schnittstellen, internen Datenstrukturen und Speicherdetails
|
||||
- jedes Modul definiert eine explizite oeffentliche Schnittstelle fuer andere Module
|
||||
- jedes Modul besitzt definierte Owned Artefakte und Schreibrechte
|
||||
- interne Implementierungen anderer Module duerfen nicht direkt genutzt werden
|
||||
- jede Modulgrenze muss als potenzielle spaetere Service-Grenze behandelbar bleiben
|
||||
|
||||
### 2.2 Prozesse
|
||||
|
||||
Ein Prozess ist ein sequenzieller Hauptablauf innerhalb genau eines owning Hauptmoduls.
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- ein Hauptmodul besitzt beliebig viele Hauptprozesse; jeder Hauptprozess gehoert genau einem Hauptmodul
|
||||
- ein Hauptprozess darf Submodule desselben Hauptmoduls nutzen
|
||||
- fachliche Hauptprozesse ueber Hauptmodulgrenzen hinweg sind nicht zulaessig
|
||||
- Batching, Parallelisierung, Retry, Resume, Fehlerklassifikation, Statusermittlung und Ergebnisvertrag gehoeren in den owning Prozessvertrag
|
||||
- zusaetzliche harte Schreib-, Feld- oder Datenbankschranken sind nur zulaessig, wenn sie explizit im Prozess- oder DB-Vertrag festgelegt sind
|
||||
|
||||
Verbindliche Kettenarten:
|
||||
|
||||
- asynchrone Prozessketten: ein Startprozess stoesst Folgeprozesse an, diese laufen als eigene Runs weiter
|
||||
- synchrone Prozessketten: Subprozesse haengen direkt aneinander; jeder Schritt stoesst nur den naechsten an
|
||||
- der Kettenmodus muss pro Prozessvertrag explizit festgelegt sein
|
||||
- die Wahl des Kettenmodus erfolgt jeweils nach fachlichem Sinn und Prozessvertrag
|
||||
|
||||
Aktive Prozesswahrheit:
|
||||
|
||||
`docs/modules/<modul>/<submodul>/processes/`
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- Prozess- und Sub-Prozess-Dokumente unter diesem Pfad sind die aktive fachliche Wahrheit
|
||||
- Prozessnomenklatur folgt `component.subcomponent.process_name`
|
||||
- jeder Hauptprozess und jeder Sub-Prozess besitzt genau einen owning Modulpfad
|
||||
- `docs/system_components/` darf fuer aktive Prozessreferenzen nicht verwendet werden
|
||||
|
||||
### 2.3 Implementierung
|
||||
|
||||
Module, Prozesse und Sub-Prozesse sind technisch so scharf wie moeglich zu trennen.
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- jedes Modul besitzt eine interne API oder Zugriffsschicht
|
||||
- Cross-Modul-Kommunikation erfolgt nur ueber definierte Modul-Schnittstellen
|
||||
- direkte Zugriffe auf interne Implementierungen oder Daten anderer Module sind verboten
|
||||
- neue APIs im geschuetzten Bereich sind standardmaessig `protected`; die Abo-Pruefung greift vor jeder Datenlieferung
|
||||
- Module, Prozesse und Sub-Prozesse muessen getrennt entwickelbar, testbar, ausrollbar, ueberwachbar, debugbar und optimierbar sein
|
||||
- technische Kopplung zwischen Prozessen und Sub-Prozessen ist auf das notwendige Minimum zu reduzieren
|
||||
- Module duerfen keine impliziten Abhaengigkeiten besitzen, die eine spaetere Extraktion verhindern
|
||||
|
||||
### 2.4 Frontend
|
||||
|
||||
Das Frontend ist bewusst minimalistisch. Ziel ist eine kleine Zahl konsistenter, wiederverwendbarer UI-Bausteine statt vieler seiten- oder feature-spezifischer Sonderkomponenten.
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- Seiten und Views komponieren Daten, Layouts und Komponenten, erzeugen aber keine Fachlogik
|
||||
- UI-Komponenten duerfen keine fachliche Primaerlogik, fachliche Dateninterpretation oder eigenstaendige Fachwahrheit besitzen
|
||||
- fachliche UI-Komponenten, fachliche Darstellungsregeln und fachlich interpretierende UI-Logik gehoeren zum owning Fachmodul
|
||||
- fachlich neutrale UI-Komponenten, Layout-Komponenten, Formatierungshelfer, Interaktionsmuster und Frontend-Utilities gehoeren nach `shared`
|
||||
- neue oder geaenderte Frontend-Komponenten muessen zuerst gegen bestehende Komponenten, Patterns und `shared`-Bausteine geprueft werden
|
||||
- Varianten bestehender Komponenten sind ueber Props, Konfiguration oder dokumentierte Erweiterungspunkte umzusetzen, nicht durch Copy-Paste
|
||||
- Sortierung, Filterung und Suche werden serverseitig im owning Modul bzw. in der API umgesetzt; das Frontend rendert nur die Ergebnisse
|
||||
- Karten, Tabellen, Filter, Tabs, Buttons, Statusanzeigen, Ladezustaende, Empty States, Fehlermeldungen und Detailansichten sind als wiederverwendbare Patterns zu behandeln
|
||||
- seitenlokale Sonderkomponenten sind nur zulaessig, wenn sie nachweislich nicht sinnvoll wiederverwendbar sind
|
||||
- API-Responses duerfen frontendnah komponiert sein, aber keine Fachlogik aus dem owning Modul in API oder Frontend verschieben
|
||||
- Seiten mit externen Redirect- oder Callback-Flows muessen Browser-History und Ruecksprungverhalten bewusst behandeln; der verbindliche Detailvertrag liegt zentral unter `docs/modules/shared/ui_foundation/`.
|
||||
|
||||
### 2.4.1 Mehrsprachigkeit
|
||||
|
||||
Mehrsprachigkeit ist ein verbindlicher Teil der Frontend-Architektur und gilt identisch fuer statische Texte, dynamisch gerenderte Texte und interaktive UI-Elemente.
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- jeder sichtbare GUI-String muss ueber einen i18n-Key aufgeloest werden, unabhaengig davon, ob er serverseitig, clientseitig oder aus einer API-Komposition gerendert wird
|
||||
- dazu gehoeren insbesondere Navigationslabels, Tab-Titel, Buttontexte, Menuepunkte, Tooltips, Hilfetexte, Platzhalter, Empty States, Ladezustände, Fehlermeldungen, Bestätigungsdialoge, ARIA-Labels und sonstige Screenreader-Texte
|
||||
- source-code-Defaults sind nur Fallback- bzw. Seed-Werte; die Anzeige-Wahrheit wird pro Locale aus dem i18n-Store bezogen
|
||||
- interaktive Elemente muessen sichtbare Beschriftung und funktionalen Wert strikt trennen: der angezeigte Text ist uebersetzbar, der Wert fuer State, Transport, Routing, Filterung und Backend-Verarbeitung bleibt technisch stabil und locale-unabhaengig
|
||||
- Auswahlwerte, Keys, IDs, Data-Attribute, Option-Keys, Tab-Keys und sonstige Steuerungswerte duerfen nicht aus uebersetzten Labels abgeleitet werden
|
||||
- Locale-Wechsel darf ausschliesslich die Anzeigeschicht wechseln; Funktion, Auswahlzustand, Navigation, Filterlogik und Datenuebergabe muessen identisch weiter funktionieren
|
||||
- wenn Text im DOM sichtbar ist, gilt er als GUI-String und unterliegt dieser Regel auch dann, wenn er clientseitig zusammengesetzt wird
|
||||
- zusammengesetzte UI-Texte duerfen keine fachliche Logik enthalten; sie kombinieren nur locale-gebundene Textbausteine mit stabilen technischen Werten
|
||||
|
||||
## 3. Modulmodell
|
||||
|
||||
### 3.1 Zulaessige Modulbereiche
|
||||
|
||||
Fachliche Hauptmodule:
|
||||
|
||||
- `company`
|
||||
- `ratings`
|
||||
- `groups`
|
||||
- `lists`
|
||||
|
||||
Technische Modulbereiche:
|
||||
|
||||
- `system`
|
||||
- `shared`
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- fachliche Hauptmodule besitzen Business-Logik und fachliche Ownership
|
||||
- `system` enthaelt nur technische Runtime-, Start-, Trigger- und Laufzeitlogik
|
||||
- `shared` enthaelt nur fachlich neutrale, wiederverwendbare technische Bausteine
|
||||
- konkrete Submodule, Prozesse, Artefakte und Schnittstellen werden in Modulvertraegen dokumentiert, nicht in diesem Architektur-Master
|
||||
|
||||
### 3.2 `shared`
|
||||
|
||||
`shared` ist der zentrale technische Querschnittsbereich.
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- `shared` darf keine Business-Logik, Fachwahrheit oder primaere fachliche Ownership besitzen
|
||||
- `shared` darf Services, Komponenten, Helper, technische Schnittstellen, UI-nahe und API-nahe Bausteine bereitstellen
|
||||
- ein Baustein gehoert nur nach `shared`, wenn er fachlich neutral ist und keine Verantwortung eines Fachmoduls uebernimmt
|
||||
- fachliche Regeln, Dateninterpretation, Statuslogik und Entscheidungen bleiben im owning Fachmodul
|
||||
- technische Querschnittslogik ohne fachlichen Inhalt muss in `shared` oder einem dort dokumentierten zentralen Baustein liegen, sobald Wiederverwendung ueber mehr als eine Stelle absehbar ist
|
||||
|
||||
### 3.3 `admin`
|
||||
|
||||
`admin` ist kein Hauptmodul und besitzt keine fachliche Ownership.
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- Admin-Funktionen sind nur Bedienoberflaechen auf bestehende Hauptmodule
|
||||
- Admin-spezifische Fachlogik liegt im owning Hauptmodul
|
||||
- ein separates fachliches Modul `admin` ist nicht zulaessig
|
||||
|
||||
### 3.4 `public/api/`
|
||||
|
||||
`public/api/` ist die HTTP-Service- und Kompositionsschicht des modularen Monolithen.
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- `public/api/` besitzt keine fachliche Primaerverantwortung
|
||||
- `public/api/` darf Modul- oder Submodul-Schnittstellen fuer konkrete Frontend-User-Stories zusammensetzen
|
||||
- `public/api/` bleibt duenn und enthaelt nur HTTP-nahe und technische Kompositionslogik
|
||||
- fachliche Ownership darf nicht aus Modulen in `public/api/` wandern
|
||||
|
||||
## 4. Datenbankgrenze
|
||||
|
||||
Die Plattform nutzt aktuell genau eine gemeinsame Datenbank. Diese gemeinsame Datenbank hebt Modul- und Ownership-Grenzen nicht auf.
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- jede Tabelle, View, materialisierte View und DB-Funktion besitzt genau eine primaere Ownership
|
||||
- primaere Ownership folgt genau einem Hauptmodul oder einem technischen Shared-Bereich
|
||||
- fachliche Beziehungen sowie Ownership- und Berechtigungsschluessel verwenden stabile interne Entitaets-IDs; veraenderliche Attribute wie E-Mail, Username oder Anzeigename duerfen dafuer nicht verwendet werden
|
||||
- nur das owning Modul definiert Struktur, Write-Logik, Constraints und fachliche Semantik eines DB-Artefakts
|
||||
- direkte Cross-Modul-Writes in fremde Owned Tabellen sind verboten
|
||||
- andere Module duerfen fremde Artefakte nur ueber definierte Schnittstellen des owning Moduls nutzen
|
||||
- physische DB-Splits sind kein aktueller Standard und beduerfen einer expliziten Architekturentscheidung
|
||||
|
||||
Operative DB-Referenz:
|
||||
|
||||
`docs/database/module_db_ownership.md`
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- konkrete DB-Artefaktlisten gehoeren in die DB-Ownership-Dokumentation, nicht in diesen Architektur-Master
|
||||
- bei Analyse, Refactoring und Implementierung wird der DB-Scope zuerst ueber Modul plus Owned DB-Artefakte bestimmt
|
||||
- fremde DB-Artefakte duerfen nur bei deklarierter Leseabhaengigkeit, FK-Beziehung oder Integrationspruefung in den Scope aufgenommen werden
|
||||
|
||||
## 5. Dokumentation und Lesescope
|
||||
|
||||
Aktive Dokumentation ist komponentennah.
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- aktive Modul- und Submodul-Dokumentation liegt unter `docs/modules/<modul>/...`
|
||||
- aktive technische Shared-Dokumentation liegt unter `docs/modules/shared/...`
|
||||
- technische Runtime-Dokumentation liegt unter `docs/modules/system/...`
|
||||
- fachliche Hauptprozesse liegen innerhalb des owning Modulbaums
|
||||
- historische oder abgeloeste Dokumentation liegt ausserhalb des aktiven Modulbaums
|
||||
- `docs/technical_architecture.md` bleibt der einzige aktive Architektur-Master
|
||||
|
||||
Standard-Lesescope:
|
||||
|
||||
- Arbeit an einem Modul: `docs/technical_architecture.md` plus `docs/modules/<modul>/...`
|
||||
- Arbeit an einem Submodul: `docs/technical_architecture.md` plus `docs/modules/<modul>/<submodul>/...`
|
||||
- `docs/modules/shared/...`: nur bei betroffener Shared-Schnittstelle oder moeglicher Wiederverwendung
|
||||
- `docs/modules/system/...`: nur bei Runtime-, Queue-, Scheduler-, Trigger- oder Supervisor-Themen
|
||||
- andere Module: nur bei explizitem Integrationsbedarf
|
||||
|
||||
## 6. Wiederverwendung und Redundanzverbot
|
||||
|
||||
Vor jeder Aenderung an App, Frontend, UI-Komponenten, UI-Patterns, API, Prozesslogik, Datenzugriff, Infrastruktur oder Dokumentation ist Wiederverwendung vor Neuerstellung zu pruefen.
|
||||
|
||||
Ziel ist eine Architektur ohne redundante Fachlogik, technische Inselloesungen, doppelte UI-Komponenten oder mehrfach gepflegte Varianten desselben Musters.
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- zuerst bestehende Muster, Komponenten, Services, Modul-Schnittstellen und `shared`-Bausteine pruefen
|
||||
- vorhandene passende Muster muessen wiederverwendet, erweitert oder als zentrale Schnittstelle bereitgestellt werden
|
||||
- fachliche Inhalte gehoeren immer in das owning Fachmodul und werden von dort ueber definierte Schnittstellen bereitgestellt
|
||||
- fachliche Logik darf nicht in `shared`, `public/api/`, `admin`, Skripte oder fachfremde Bereiche ausgelagert oder dupliziert werden
|
||||
- fachlich neutrale technische Logik gehoert nach `shared`, sofern sie wiederverwendbar ist oder Wiederverwendung absehbar ist
|
||||
- Frontend-Darstellung, Interaktionslogik, Layout, Formatierung und Zustandsdarstellung sind als bestehende oder neue zentrale UI-Komponenten beziehungsweise UI-Patterns in `shared` zu pruefen
|
||||
- neue `shared`-Bausteine sind nur zulaessig, wenn sie keine Fachwahrheit enthalten, keine fachliche Primaerverantwortung uebernehmen und wiederverwendbar sind
|
||||
- technische Querschnittsbausteine duerfen Module entkoppeln, aber keine fachlichen Entscheidungen aus owning Modulen herausziehen
|
||||
- neue Loesungen sind nur zulaessig, wenn kein passendes bestehendes Muster, keine passende Modul-Schnittstelle und kein geeigneter `shared`-Baustein existiert
|
||||
- Quickfixes, Workarounds, Copy-Paste-Varianten, seitenlokale UI-Duplikate und isolierte Sonderlogik fuer Einzelstellen sind verboten
|
||||
- Redundanzvermeidung muss bei Analyse, Umsetzung, Review und Refactoring aktiv nachgewiesen werden koennen
|
||||
|
||||
## 7. Verbotene Strukturen
|
||||
|
||||
Nicht zulaessig sind:
|
||||
|
||||
- Module als reine Ordner ohne Contract
|
||||
- direkte Cross-Modul-Zugriffe auf DB-Tabellen, interne Implementierungen oder Daten anderer Module
|
||||
- versteckte oder implizite Modulabhaengigkeiten
|
||||
- Vermischung mehrerer Primaerverantwortungen innerhalb eines Moduls
|
||||
- parallele Fachlogik ausserhalb des owning Moduls
|
||||
- redundante fachliche, technische oder UI-Loesungen, obwohl ein bestehendes Modul, eine Schnittstelle, ein Pattern oder ein `shared`-Baustein erweitert werden koennte
|
||||
- neue generische Frameworks oder Orchestrator-Umbauten ohne explizite Architekturentscheidung
|
||||
- implizite Schutz-, Schreib- oder Datenbankschranken ohne Prozess- oder DB-Vertrag
|
||||
|
||||
## 8. Erweiterungsregeln
|
||||
|
||||
Neue Hauptmodule, Submodule oder zentrale Bausteine duerfen nur eingefuehrt werden, wenn bestehende Strukturen die Verantwortung nicht sauber tragen koennen.
|
||||
|
||||
Prueffragen vor jeder Erweiterung:
|
||||
|
||||
1. Welches bestehende Hauptmodul ist primaer betroffen?
|
||||
2. Reicht eine neue Datei, ein neues Submodul oder eine neue Schnittstelle im owning Hauptmodul aus?
|
||||
3. Handelt es sich um fachlich neutrale Wiederverwendung, die nach `shared` gehoert?
|
||||
4. Entsteht wirklich eine neue Primaerverantwortung mit eigenen Owned Artefakten und Schreibrechten?
|
||||
5. Bleibt der Handshake zum Rest des Systems auf minimale Scope-Identifier begrenzt?
|
||||
6. Reduziert der neue Schnitt Kopplung und Redundanz, statt sie zu erhoehen?
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- neue Hauptmodule sind nur zulaessig, wenn eine neue fachliche oder technische Primaerverantwortung entsteht, die nicht sauber in `company`, `ratings`, `groups`, `lists`, `system` oder `shared` aufgeht
|
||||
- neue Submodule sind nur zulaessig, wenn innerhalb eines bestehenden Hauptmoduls eine klar isolierbare Verantwortung mit eigener Schnittstelle entsteht
|
||||
- neue Prozess- und Sub-Prozess-Dokumente werden im owning Modulpfad angelegt und folgen `component.subcomponent.process_name`
|
||||
- `admin`, `public/api/`, `scripts/` oder `lib/` begruenden kein neues fachliches Hauptmodul
|
||||
- Bequemlichkeit, Dateigroesse oder kurzfristiger Umbauaufwand rechtfertigen kein neues Hauptmodul
|
||||
- vor jeder normativen Architektur- oder Modulerweiterung muss dieses Dokument aktualisiert werden
|
||||
|
||||
## 9. Aenderungsregel
|
||||
|
||||
Jede strukturelle Aenderung an Hauptmodulen, Modulgrenzen, Prozesszuordnungen, Frontend-Prinzipien, API-Grenzen, DB-Ownership oder primaeren Ownership-Prinzipien muss zuerst in diesem Dokument normativ festgelegt und danach in Detaildokumenten nachgezogen werden.
|
||||
|
||||
Verbindlich ist:
|
||||
|
||||
- `docs/technical_architecture.md` ist der einzige aktive Architektur-Master des Repositories
|
||||
- andere Dokumente duerfen diese Architektur erklaeren, anwenden oder historisieren, aber keine konkurrierende Master-Struktur definieren
|
||||
- konkrete Inventare wie Tabellenlisten, Submodullisten, Prozesslisten oder Runtime-Artefakte werden in Detaildokumenten gepflegt und nicht hier dupliziert
|
||||
|
||||
## 10. Projektspezifische Architekturvorgaben
|
||||
|
||||
Dieses Kapitel enthaelt Architekturentscheidungen, die fuer dieses Projekt
|
||||
gelten und nicht Teil der allgemeinen, projektuebergreifend wiederverwendbaren
|
||||
Modularitaet sind.
|
||||
|
||||
### 10.1 Fachliches Hauptmodul `market_management`
|
||||
|
||||
Dieses Projekt besitzt zusaetzlich das fachliche Hauptmodul
|
||||
`market_management`. Seine fachliche Ownership, Datenstruktur und
|
||||
Schnittstellen werden in den Modulvertraegen unter
|
||||
`docs/modules/market_management/` dokumentiert.
|
||||
|
||||
### 10.2 GUI-/MCP-Paritaet
|
||||
|
||||
Die MCP-Schnittstelle ist im Grundsatz funktional paritaetisch zur GUI. Ein
|
||||
MCP-Nutzer ist hinsichtlich der fachlichen Nutzung einem GUI-Nutzer
|
||||
gleichzustellen.
|
||||
|
||||
Verbindliche Regel:
|
||||
|
||||
- jede fachliche Funktion, die technisch ueber das GUI-Interface verfuegbar
|
||||
ist, muss auch ueber die MCP-Schnittstelle ausfuehrbar sein
|
||||
- die Paritaet bezieht sich auf die technischen Moeglichkeiten des Interfaces,
|
||||
nicht nur auf aktuell im GUI dargestellte Funktionen; technisch vorhandene,
|
||||
im GUI nicht angezeigte Funktionen muessen trotzdem ueber MCP verfuegbar sein
|
||||
- alle fachlichen Lese-, Anlege-, Aenderungs-, Archivierungs- und
|
||||
Reaktivierungsmoeglichkeiten des GUI-Interfaces muessen im MCP-Interface
|
||||
abgebildet sein
|
||||
- GUI und MCP verwenden dieselben fachlichen Validierungen, Berechtigungen,
|
||||
Statusregeln und Ownership-Grenzen; fachliche Logik darf nicht doppelt in
|
||||
GUI und MCP implementiert werden
|
||||
- jede Erweiterung oder Aenderung eines GUI-Anwendungsfalls erfordert zwingend
|
||||
die gleichzeitige Pruefung und, falls erforderlich, Anpassung des MCP-Interfaces
|
||||
- ein MCP-Nutzer darf gegenueber einem gleich berechtigten GUI-Nutzer weder
|
||||
fachlich eingeschraenkt noch durch abweichende Regeln bevorzugt werden
|
||||
- die Paritaet betrifft die fachliche Funktion und den Datenzugriff, nicht die
|
||||
visuelle Darstellung oder den Interaktionsablauf der GUI
|
||||
@@ -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();
|
||||
@@ -390,6 +394,8 @@ function get_sales_order_detail(PDO $pdo, int $orderId): ?array
|
||||
so.amount_discount,
|
||||
so.total_amount,
|
||||
so.currency,
|
||||
md.name AS market_name,
|
||||
mday.date AS market_occurrence_day_date,
|
||||
COALESCE(ad.first_name, '') AS first_name,
|
||||
COALESCE(ad.last_name, '') AS last_name,
|
||||
COALESCE(ad.street, '') AS street,
|
||||
@@ -409,6 +415,10 @@ function get_sales_order_detail(PDO $pdo, int $orderId): ?array
|
||||
) ad ON TRUE
|
||||
LEFT JOIN payment_method pm ON pm.id = so.payment_method_id
|
||||
LEFT JOIN shipping_method sm ON sm.id = so.shipping_method_id
|
||||
LEFT JOIN market_occurrence mo ON mo.id = so.market_occurrence_id
|
||||
LEFT JOIN market_definition md ON md.id = mo.market_definition_id
|
||||
LEFT JOIN market_occurrence_day mday
|
||||
ON mday.id = so.market_occurrence_day_id AND mday.occurrence_id = mo.id
|
||||
WHERE so.id = :order_id"
|
||||
);
|
||||
$orderStmt->execute([':order_id' => $orderId]);
|
||||
@@ -495,6 +505,10 @@ function get_sales_order_detail(PDO $pdo, int $orderId): ?array
|
||||
'payment_status' => (string) $order['payment_status'],
|
||||
'payment_method' => (string) $order['payment_method'],
|
||||
'shipping_method' => (string) $order['shipping_method'],
|
||||
'market' => [
|
||||
'name' => $order['market_name'] !== null ? (string) $order['market_name'] : null,
|
||||
'occurrence_day_date' => $order['market_occurrence_day_date'] !== null ? (string) $order['market_occurrence_day_date'] : null,
|
||||
],
|
||||
'customer' => [
|
||||
'first_name' => (string) $order['first_name'],
|
||||
'last_name' => (string) $order['last_name'],
|
||||
|
||||
@@ -41,6 +41,20 @@ $products = $data['products'];
|
||||
$orderDate = trim((string) $data['orderDate']);
|
||||
$payments = $data['payments'];
|
||||
$billing = is_array($data['billing']) ? $data['billing'] : [];
|
||||
$marketOccurrenceId = $data['marketOccurrenceId'] ?? null;
|
||||
$marketOccurrenceDayId = $data['marketOccurrenceDayId'] ?? null;
|
||||
|
||||
if (($marketOccurrenceId === null) !== ($marketOccurrenceDayId === null)) {
|
||||
json_response(422, ['ok' => false, 'error' => 'Markt und Markttag müssen gemeinsam angegeben werden.']);
|
||||
}
|
||||
|
||||
if ($marketOccurrenceId !== null && (!ctype_digit((string) $marketOccurrenceId) || (int) $marketOccurrenceId <= 0)) {
|
||||
json_response(422, ['ok' => false, 'error' => 'Ungültiger Markt.']);
|
||||
}
|
||||
|
||||
if ($marketOccurrenceDayId !== null && (!ctype_digit((string) $marketOccurrenceDayId) || (int) $marketOccurrenceDayId <= 0)) {
|
||||
json_response(422, ['ok' => false, 'error' => 'Ungültiger Markttag.']);
|
||||
}
|
||||
|
||||
if (!is_array($products) || count($products) === 0) {
|
||||
json_response(422, ['ok' => false, 'error' => 'No products specified']);
|
||||
@@ -115,6 +129,27 @@ try {
|
||||
$pdo = connect_database($env);
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($marketOccurrenceId !== null) {
|
||||
$marketLinkStmt = $pdo->prepare(
|
||||
'SELECT 1
|
||||
FROM market_occurrence o
|
||||
INNER JOIN market_definition m ON m.id = o.market_definition_id
|
||||
INNER JOIN market_occurrence_day d ON d.occurrence_id = o.id
|
||||
WHERE o.id = :occurrence_id
|
||||
AND d.id = :day_id
|
||||
AND m.archived_at IS NULL
|
||||
AND o.archived_at IS NULL
|
||||
LIMIT 1'
|
||||
);
|
||||
$marketLinkStmt->execute([
|
||||
':occurrence_id' => (int) $marketOccurrenceId,
|
||||
':day_id' => (int) $marketOccurrenceDayId,
|
||||
]);
|
||||
if ($marketLinkStmt->fetchColumn() === false) {
|
||||
throw new InvalidArgumentException('Der Markt oder der Markttag ist nicht aktiv oder nicht miteinander verknüpft.');
|
||||
}
|
||||
}
|
||||
|
||||
$locations = get_default_location_ids($pdo);
|
||||
|
||||
$paymentMethodIds = [];
|
||||
@@ -135,6 +170,8 @@ try {
|
||||
':amount_net' => $totalPrice,
|
||||
':total_amount' => $totalPrice,
|
||||
':order_date' => $orderDateTime->format('Y-m-d H:i:s'),
|
||||
':market_occurrence_id' => $marketOccurrenceId === null ? null : (int) $marketOccurrenceId,
|
||||
':market_occurrence_day_id' => $marketOccurrenceDayId === null ? null : (int) $marketOccurrenceDayId,
|
||||
]);
|
||||
|
||||
$orderId = (int) $order['id'];
|
||||
@@ -221,7 +258,16 @@ try {
|
||||
'excelTrigger' => $excelTrigger,
|
||||
'message' => 'OTC order created successfully',
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
} catch (InvalidArgumentException $e) {
|
||||
if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
json_response(422, [
|
||||
'ok' => false,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
@@ -125,6 +125,101 @@ function get_manufacturer_options(PDO $pdo): array
|
||||
);
|
||||
}
|
||||
|
||||
function get_party_reference(PDO $pdo, int $partyId): ?array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT id, name, email, phone
|
||||
FROM party
|
||||
WHERE id = :id'
|
||||
);
|
||||
$stmt->execute([':id' => $partyId]);
|
||||
$party = $stmt->fetch();
|
||||
|
||||
return is_array($party) ? $party : null;
|
||||
}
|
||||
|
||||
function get_contact_reference(PDO $pdo, int $contactId): ?array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT id, party_id, first_name, last_name, email, phone, position_title
|
||||
FROM contact
|
||||
WHERE id = :id'
|
||||
);
|
||||
$stmt->execute([':id' => $contactId]);
|
||||
$contact = $stmt->fetch();
|
||||
|
||||
return is_array($contact) ? $contact : null;
|
||||
}
|
||||
|
||||
function get_party_options(PDO $pdo): array
|
||||
{
|
||||
$stmt = $pdo->query(
|
||||
"SELECT id, name
|
||||
FROM party
|
||||
WHERE status = 'active'
|
||||
ORDER BY name ASC, id ASC"
|
||||
);
|
||||
|
||||
return array_map(
|
||||
static fn (array $row): array => ['id' => (int) $row['id'], 'name' => (string) ($row['name'] ?? '')],
|
||||
$stmt->fetchAll()
|
||||
);
|
||||
}
|
||||
|
||||
function get_contact_options(PDO $pdo): array
|
||||
{
|
||||
$stmt = $pdo->query(
|
||||
"SELECT c.id, c.party_id, c.first_name, c.last_name, p.name AS party_name
|
||||
FROM contact c
|
||||
INNER JOIN party p ON p.id = c.party_id
|
||||
WHERE p.status = 'active'
|
||||
ORDER BY p.name ASC, c.last_name ASC, c.first_name ASC, c.id ASC"
|
||||
);
|
||||
|
||||
return array_map(
|
||||
static fn (array $row): array => [
|
||||
'id' => (int) $row['id'],
|
||||
'party_id' => (int) $row['party_id'],
|
||||
'first_name' => (string) ($row['first_name'] ?? ''),
|
||||
'last_name' => (string) ($row['last_name'] ?? ''),
|
||||
'party_name' => (string) ($row['party_name'] ?? ''),
|
||||
],
|
||||
$stmt->fetchAll()
|
||||
);
|
||||
}
|
||||
|
||||
function update_contact_details(PDO $pdo, int $contactId, array $input): array
|
||||
{
|
||||
if ($contactId <= 0) {
|
||||
throw new InvalidArgumentException('contact_person_id muss positiv sein.');
|
||||
}
|
||||
|
||||
$sets = [];
|
||||
$params = [':id' => $contactId];
|
||||
foreach (['email', 'phone'] as $field) {
|
||||
if (array_key_exists($field, $input)) {
|
||||
$sets[] = $field . ' = :' . $field;
|
||||
$value = trim((string) ($input[$field] ?? ''));
|
||||
$params[':' . $field] = $value !== '' ? $value : null;
|
||||
}
|
||||
}
|
||||
if ($sets === []) {
|
||||
throw new InvalidArgumentException('Keine änderbaren Kontaktdaten übergeben.');
|
||||
}
|
||||
|
||||
$sets[] = 'updated_at = NOW()';
|
||||
$stmt = $pdo->prepare(
|
||||
'UPDATE contact SET ' . implode(', ', $sets) . ' WHERE id = :id
|
||||
RETURNING id, party_id, first_name, last_name, email, phone, position_title'
|
||||
);
|
||||
$stmt->execute($params);
|
||||
$contact = $stmt->fetch();
|
||||
if (!is_array($contact)) {
|
||||
throw new RuntimeException('Kontaktperson nicht gefunden.');
|
||||
}
|
||||
return $contact;
|
||||
}
|
||||
|
||||
function create_manufacturer(PDO $pdo, string $name, ?string $email = null, ?string $phone = null): array
|
||||
{
|
||||
$name = trim($name);
|
||||
|
||||
@@ -131,13 +131,15 @@ function lager_forecast_calculate(
|
||||
?DateTimeImmutable $forecastStartDate = null
|
||||
): array
|
||||
{
|
||||
$lookbackDays = 365;
|
||||
$firstSaleDate = empty($dailySales) ? null : min(array_keys($dailySales));
|
||||
$lookbackDays = $firstSaleDate === null
|
||||
? 0
|
||||
: $asOfDate->diff(new DateTimeImmutable($firstSaleDate))->days + 1;
|
||||
$halfLifeDays = 60;
|
||||
$weightedUnits = 0.0;
|
||||
$weightTotal = 0.0;
|
||||
$salesUnits = 0.0;
|
||||
$salesDays = 0;
|
||||
$firstSaleDate = null;
|
||||
$lastSaleDate = null;
|
||||
|
||||
for ($age = 0; $age < $lookbackDays; $age++) {
|
||||
@@ -150,7 +152,6 @@ function lager_forecast_calculate(
|
||||
if ($units > 0) {
|
||||
$salesUnits += $units;
|
||||
$salesDays++;
|
||||
$firstSaleDate = $dateKey;
|
||||
$lastSaleDate ??= $dateKey;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/service.php';
|
||||
|
||||
function erp_lager_mcp_token(array $env): string
|
||||
{
|
||||
$token = env_value('MARKET_MCP_TOKEN', $env);
|
||||
if ($token === '') {
|
||||
throw new RuntimeException('MARKET_MCP_TOKEN ist nicht konfiguriert.');
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
function erp_lager_mcp_authorize(array $env): string
|
||||
{
|
||||
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||
if (!preg_match('/^Bearer\s+(.+)$/i', $header, $matches)) {
|
||||
throw new RuntimeException('MCP Authorization Bearer Token fehlt.');
|
||||
}
|
||||
|
||||
$token = trim($matches[1]);
|
||||
if (!hash_equals(erp_lager_mcp_token($env), $token)) {
|
||||
throw new RuntimeException('MCP Authorization Bearer Token ist ungültig.');
|
||||
}
|
||||
|
||||
return 'hermes';
|
||||
}
|
||||
|
||||
function erp_lager_mcp_tools(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => 'erp_lager.list_products',
|
||||
'description' => 'Aktive ERP-Lagerprodukte zur Ermittlung der Produkt-IDs lesen',
|
||||
'inputSchema' => ['type' => 'object', 'properties' => (object) []],
|
||||
],
|
||||
[
|
||||
'name' => 'erp_lager.get_product',
|
||||
'description' => 'Produkt, aktuelle Lagermenge, Chargen und Ausverkaufsprognose lesen',
|
||||
'inputSchema' => [
|
||||
'type' => 'object',
|
||||
'required' => ['product_id'],
|
||||
'properties' => [
|
||||
'product_id' => ['type' => 'integer'],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function erp_lager_mcp_list_products(PDO $pdo): array
|
||||
{
|
||||
return array_map(
|
||||
static fn (array $product): array => [
|
||||
'id' => $product['id'],
|
||||
'name' => $product['name'],
|
||||
'sku' => $product['sku'],
|
||||
'uom' => $product['uom'],
|
||||
],
|
||||
get_otc_order_form_products($pdo)
|
||||
);
|
||||
}
|
||||
|
||||
function erp_lager_mcp_get_product(PDO $pdo, int $productId): array
|
||||
{
|
||||
if ($productId < 1) {
|
||||
throw new InvalidArgumentException('product_id muss eine positive Ganzzahl sein.');
|
||||
}
|
||||
|
||||
foreach (get_lager_product_overview($pdo) as $product) {
|
||||
if ((int) $product['id'] === $productId) {
|
||||
return $product;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Aktives Lagerprodukt nicht gefunden.');
|
||||
}
|
||||
|
||||
function erp_lager_mcp_call(PDO $pdo, string $tool, array $arguments): array
|
||||
{
|
||||
return match ($tool) {
|
||||
'erp_lager.list_products' => erp_lager_mcp_list_products($pdo),
|
||||
'erp_lager.get_product' => erp_lager_mcp_get_product(
|
||||
$pdo,
|
||||
(int) ($arguments['product_id'] ?? 0)
|
||||
),
|
||||
default => throw new InvalidArgumentException('Unbekanntes Lager-MCP-Tool: ' . $tool),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/service.php';
|
||||
|
||||
function market_management_mcp_token(array $env): string
|
||||
{
|
||||
$token = env_value('MARKET_MCP_TOKEN', $env);
|
||||
if ($token === '') {
|
||||
throw new RuntimeException('MARKET_MCP_TOKEN ist nicht konfiguriert.');
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
|
||||
function market_management_mcp_authorize(array $env): string
|
||||
{
|
||||
$header = (string) ($_SERVER['HTTP_AUTHORIZATION'] ?? '');
|
||||
if (!preg_match('/^Bearer\s+(.+)$/i', trim($header), $matches)) {
|
||||
throw new RuntimeException('Bearer-Token fehlt.');
|
||||
}
|
||||
|
||||
$token = trim($matches[1]);
|
||||
$expected = market_management_mcp_token($env);
|
||||
if ($token === '' || !hash_equals($expected, $token)) {
|
||||
throw new RuntimeException('Bearer-Token ist ungültig.');
|
||||
}
|
||||
|
||||
return 'bearer:' . hash('sha256', $token);
|
||||
}
|
||||
|
||||
function market_management_mcp_tools(): array
|
||||
{
|
||||
$idempotencyProperty = ['idempotency_key' => ['type' => 'string', 'minLength' => 1]];
|
||||
$occurrenceProperties = [
|
||||
'status' => ['type' => 'string'], 'place' => ['type' => ['string', 'null']], 'address' => ['type' => ['string', 'null']],
|
||||
'municipality_reference_id' => ['type' => ['integer', 'null']], 'notes' => ['type' => ['string', 'null']],
|
||||
'registration_deadline' => ['type' => ['string', 'null'], 'format' => 'date'],
|
||||
'request_or_registration_date' => ['type' => ['string', 'null'], 'format' => 'date'],
|
||||
'registration_note' => ['type' => ['string', 'null']],
|
||||
'confirmation_date' => ['type' => ['string', 'null'], 'format' => 'date'],
|
||||
'payment_deadline' => ['type' => ['string', 'null'], 'format' => 'date'],
|
||||
'payment_status' => ['type' => ['string', 'null'], 'enum' => ['paid', 'unpaid']],
|
||||
'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']],
|
||||
'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', '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', 'idempotency_key'], 'properties' => $idempotencyProperty + ['name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null']]]]],
|
||||
['name' => 'market_management.update_market', 'description' => 'Markt inklusive Veranstalter und Website-URL ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['market_id' => ['type' => 'integer'], 'name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null']]]]],
|
||||
['name' => 'market_management.archive_market', 'description' => 'Markt archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['market_id' => ['type' => 'integer']]]],
|
||||
['name' => 'market_management.reactivate_market', 'description' => 'Markt reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['market_id' => ['type' => 'integer']]]],
|
||||
['name' => 'market_management.list_categories', 'description' => 'Bestehende Kategorien lesen', 'inputSchema' => ['type' => 'object', 'properties' => (object) []]],
|
||||
['name' => 'market_management.list_market_organizers', 'description' => 'Aktive Veranstalter lesen', 'inputSchema' => ['type' => 'object', 'properties' => (object) []]],
|
||||
['name' => 'market_management.create_market_organizer', 'description' => 'Veranstalter mit optionalen Kontaktdaten anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'idempotency_key'], 'properties' => $idempotencyProperty + ['name' => ['type' => 'string'], 'email' => ['type' => ['string', 'null'], 'format' => 'email'], 'phone' => ['type' => ['string', 'null']]]]],
|
||||
['name' => 'market_management.list_reference_data', 'description' => 'Kategorien, Kantone und Gemeinden lesen', 'inputSchema' => ['type' => 'object', 'properties' => (object) []]],
|
||||
['name' => 'market_management.create_municipality', 'description' => 'Lokale Gemeinde im Markt-Gemeinderegister anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'idempotency_key'], 'properties' => $idempotencyProperty + ['name' => ['type' => 'string']]]],
|
||||
['name' => 'market_management.get_overview', 'description' => 'Marktübersicht mit serverseitigen Filtern lesen', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]],
|
||||
['name' => 'market_management.create_occurrence', 'description' => 'Marktdurchführung mit mindestens einem Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id', 'days', 'idempotency_key'], 'properties' => $idempotencyProperty + ['market_definition_id' => ['type' => 'integer'], 'status' => ['type' => 'string']] + $occurrenceProperties + ['days' => ['type' => 'array', 'minItems' => 1, 'items' => ['type' => 'object', 'required' => ['date'], 'properties' => $dayProperties]]]]],
|
||||
['name' => 'market_management.get_occurrence', 'description' => 'Marktdurchführung mit Markt, Markttagen und berechneten Verkaufsdaten lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]],
|
||||
['name' => 'market_management.get_occurrence_checklist', 'description' => 'Checkliste einer Marktdurchführung lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]],
|
||||
['name' => 'market_management.update_occurrence_checklist', 'description' => 'Checkliste einer Marktdurchführung vollständig ersetzen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'items', 'idempotency_key'], 'properties' => $idempotencyProperty + ['occurrence_id' => ['type' => 'integer'], 'comment' => ['type' => ['string', 'null']], 'items' => ['type' => 'array', 'items' => ['type' => 'object', 'required' => ['chapter_key', 'label', 'is_completed'], 'properties' => ['chapter_key' => ['type' => 'string', 'enum' => ['products_sales', 'decoration', 'stand_equipment', 'technology']], 'label' => ['type' => 'string'], 'is_completed' => ['type' => 'boolean']]]]]]],
|
||||
['name' => 'market_management.update_occurrence', 'description' => 'Marktdurchführung ändern', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['occurrence_id' => ['type' => 'integer']] + $occurrenceProperties]],
|
||||
['name' => 'market_management.archive_occurrence', 'description' => 'Marktdurchführung archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['occurrence_id' => ['type' => 'integer']]]],
|
||||
['name' => 'market_management.reactivate_occurrence', 'description' => 'Marktdurchführung reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['occurrence_id' => ['type' => 'integer']]]],
|
||||
['name' => 'market_management.create_day', 'description' => 'Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'date', 'idempotency_key'], 'properties' => $idempotencyProperty + ['occurrence_id' => ['type' => 'integer']] + $dayProperties]],
|
||||
['name' => 'market_management.update_day', 'description' => 'Markttag ändern', 'inputSchema' => ['type' => 'object', 'required' => ['day_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['day_id' => ['type' => 'integer']] + $dayProperties]],
|
||||
['name' => 'market_management.delete_day', 'description' => 'Markttag entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['day_id', 'idempotency_key'], 'properties' => $idempotencyProperty + ['day_id' => ['type' => 'integer']]]],
|
||||
];
|
||||
}
|
||||
|
||||
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.update_occurrence_checklist',
|
||||
'market_management.archive_occurrence', 'market_management.reactivate_occurrence',
|
||||
'market_management.create_day', 'market_management.update_day', 'market_management.delete_day',
|
||||
], true);
|
||||
}
|
||||
|
||||
function market_management_mcp_call_idempotent(PDO $pdo, string $userSubject, string $tool, array $arguments): array
|
||||
{
|
||||
if (!market_management_mcp_is_write_tool($tool)) {
|
||||
return market_management_mcp_call($pdo, $tool, $arguments);
|
||||
}
|
||||
|
||||
$idempotencyKey = trim((string) ($arguments['idempotency_key'] ?? ''));
|
||||
if ($idempotencyKey === '') {
|
||||
throw new InvalidArgumentException('Schreibende MCP-Aufrufe benötigen idempotency_key.');
|
||||
}
|
||||
$clientId = trim((string) ($arguments['client_id'] ?? 'hermes'));
|
||||
if ($clientId === '') {
|
||||
throw new InvalidArgumentException('client_id darf nicht leer sein.');
|
||||
}
|
||||
$businessArguments = $arguments;
|
||||
unset($businessArguments['idempotency_key'], $businessArguments['client_id']);
|
||||
$requestHash = hash('sha256', json_encode([
|
||||
'tool' => $tool,
|
||||
'arguments' => $businessArguments,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$existingStmt = $pdo->prepare(
|
||||
'SELECT user_subject, request_hash, result_status, result_payload
|
||||
FROM market_mcp_idempotency
|
||||
WHERE client_id = :client_id AND idempotency_key = :idempotency_key
|
||||
FOR UPDATE'
|
||||
);
|
||||
$existingStmt->execute([':client_id' => $clientId, ':idempotency_key' => $idempotencyKey]);
|
||||
$existing = $existingStmt->fetch();
|
||||
if (is_array($existing)) {
|
||||
if ((string) $existing['user_subject'] !== $userSubject || (string) $existing['request_hash'] !== $requestHash) {
|
||||
throw new InvalidArgumentException('Idempotency-Schlüssel wurde bereits für eine andere Anfrage verwendet.');
|
||||
}
|
||||
if ((string) $existing['result_status'] === 'succeeded' && $existing['result_payload'] !== null) {
|
||||
$pdo->commit();
|
||||
return (array) json_decode((string) $existing['result_payload'], true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
throw new RuntimeException('Idempotency-Anfrage wird bereits verarbeitet oder ist fehlgeschlagen.');
|
||||
}
|
||||
|
||||
$insert = $pdo->prepare(
|
||||
'INSERT INTO market_mcp_idempotency
|
||||
(user_subject, client_id, idempotency_key, tool_name, request_hash, result_status, expires_at)
|
||||
VALUES (:user_subject, :client_id, :idempotency_key, :tool_name, :request_hash, \'processing\', NOW() + INTERVAL \'24 hours\')'
|
||||
);
|
||||
$insert->execute([
|
||||
':user_subject' => $userSubject,
|
||||
':client_id' => $clientId,
|
||||
':idempotency_key' => $idempotencyKey,
|
||||
':tool_name' => $tool,
|
||||
':request_hash' => $requestHash,
|
||||
]);
|
||||
$result = market_management_mcp_call($pdo, $tool, $businessArguments);
|
||||
$update = $pdo->prepare(
|
||||
'UPDATE market_mcp_idempotency
|
||||
SET result_status = \'succeeded\', result_payload = CAST(:result_payload AS JSONB)
|
||||
WHERE client_id = :client_id AND idempotency_key = :idempotency_key'
|
||||
);
|
||||
$update->execute([
|
||||
':result_payload' => json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
|
||||
':client_id' => $clientId,
|
||||
':idempotency_key' => $idempotencyKey,
|
||||
]);
|
||||
$pdo->commit();
|
||||
return $result;
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
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_mcp_get_market(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'),
|
||||
(bool) ($arguments['include_archived'] ?? false)
|
||||
),
|
||||
'market_management.create_market' => market_management_create_market($pdo, $arguments),
|
||||
'market_management.update_market' => market_management_update_market(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'),
|
||||
$arguments
|
||||
),
|
||||
'market_management.archive_market' => market_management_set_market_archive(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'),
|
||||
true
|
||||
),
|
||||
'market_management.reactivate_market' => market_management_set_market_archive(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'),
|
||||
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' => (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)
|
||||
),
|
||||
'market_management.get_occurrence_checklist' => market_management_get_occurrence_checklist(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
|
||||
(bool) ($arguments['include_archived'] ?? false)
|
||||
),
|
||||
'market_management.update_occurrence_checklist' => market_management_update_occurrence_checklist(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
|
||||
$arguments
|
||||
),
|
||||
'market_management.update_occurrence' => market_management_mcp_update_occurrence(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
|
||||
$arguments
|
||||
),
|
||||
'market_management.archive_occurrence' => market_management_set_occurrence_archive(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
|
||||
true
|
||||
),
|
||||
'market_management.reactivate_occurrence' => market_management_set_occurrence_archive(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
|
||||
false
|
||||
),
|
||||
'market_management.create_day' => market_management_create_day($pdo, $arguments),
|
||||
'market_management.update_day' => market_management_update_day(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['day_id'] ?? null, 'day_id'),
|
||||
$arguments
|
||||
),
|
||||
'market_management.delete_day' => market_management_delete_day(
|
||||
$pdo,
|
||||
market_management_require_positive_id($arguments['day_id'] ?? null, 'day_id')
|
||||
),
|
||||
default => throw new InvalidArgumentException('Unbekanntes MCP-Tool: ' . $tool),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
const MARKET_OCCURRENCE_CHECKLIST_CHAPTERS = [
|
||||
[
|
||||
'key' => 'products_sales',
|
||||
'label' => 'Produkte & Verkauf',
|
||||
'items' => ['Fläschchen', 'Säckchen', 'Dunkle Kleber', 'Naurua-Kleber', 'Anwendungsbeilage', 'Flyer Naurua', 'Flyer Vitalpilze', 'Vitalpilzbuch', 'SumUp-Terminal', 'Bargeld'],
|
||||
],
|
||||
[
|
||||
'key' => 'decoration',
|
||||
'label' => 'Dekoration',
|
||||
'items' => ['Dekoration', 'Pilze', 'Blumenvase', 'Blume'],
|
||||
],
|
||||
[
|
||||
'key' => 'stand_equipment',
|
||||
'label' => 'Stand & Ausstattung',
|
||||
'items' => ['Tisch', 'Tischtücher', 'Beleuchtung', 'Holzklötze', 'Wagen', 'Sonnenschirm', 'Parkkarte/-Marktinformationen'],
|
||||
],
|
||||
[
|
||||
'key' => 'technology',
|
||||
'label' => 'Technik',
|
||||
'items' => ['Batterie-/Powerbank fürs Handy'],
|
||||
],
|
||||
];
|
||||
|
||||
function market_management_checklist_default_items(): array
|
||||
{
|
||||
$items = [];
|
||||
foreach (MARKET_OCCURRENCE_CHECKLIST_CHAPTERS as $chapter) {
|
||||
foreach ($chapter['items'] as $label) {
|
||||
$items[] = [
|
||||
'chapter_key' => $chapter['key'],
|
||||
'label' => $label,
|
||||
];
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
function market_management_checklist_assert_occurrence(PDO $pdo, int $occurrenceId, bool $includeArchived = false): void
|
||||
{
|
||||
$predicate = $includeArchived ? 'TRUE' : 'archived_at IS NULL';
|
||||
$stmt = $pdo->prepare('SELECT id FROM market_occurrence WHERE id = :id AND ' . $predicate);
|
||||
$stmt->execute([':id' => $occurrenceId]);
|
||||
if ($stmt->fetchColumn() === false) {
|
||||
throw new InvalidArgumentException('occurrence_id verweist nicht auf eine Marktdurchführung.');
|
||||
}
|
||||
}
|
||||
|
||||
function market_management_checklist_initialize(PDO $pdo, int $occurrenceId): void
|
||||
{
|
||||
market_management_checklist_assert_occurrence($pdo, $occurrenceId, true);
|
||||
$parent = $pdo->prepare(
|
||||
'INSERT INTO market_occurrence_checklist (occurrence_id)
|
||||
VALUES (:occurrence_id)
|
||||
ON CONFLICT (occurrence_id) DO NOTHING'
|
||||
);
|
||||
$parent->execute([':occurrence_id' => $occurrenceId]);
|
||||
|
||||
$existing = $pdo->prepare('SELECT COUNT(*) FROM market_occurrence_checklist_item WHERE occurrence_id = :occurrence_id');
|
||||
$existing->execute([':occurrence_id' => $occurrenceId]);
|
||||
if ((int) $existing->fetchColumn() > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$insert = $pdo->prepare(
|
||||
'INSERT INTO market_occurrence_checklist_item
|
||||
(occurrence_id, chapter_key, position, label, is_completed)
|
||||
VALUES (:occurrence_id, :chapter_key, :position, :label, :is_completed)'
|
||||
);
|
||||
$chapterPositions = [];
|
||||
foreach (market_management_checklist_default_items() as $item) {
|
||||
$insert->bindValue(':occurrence_id', $occurrenceId, PDO::PARAM_INT);
|
||||
$insert->bindValue(':chapter_key', $item['chapter_key'], PDO::PARAM_STR);
|
||||
$chapterPositions[$item['chapter_key']] = ($chapterPositions[$item['chapter_key']] ?? 0) + 1;
|
||||
$insert->bindValue(':position', $chapterPositions[$item['chapter_key']], PDO::PARAM_INT);
|
||||
$insert->bindValue(':label', $item['label'], PDO::PARAM_STR);
|
||||
$insert->bindValue(':is_completed', false, PDO::PARAM_BOOL);
|
||||
$insert->execute();
|
||||
}
|
||||
}
|
||||
|
||||
function market_management_get_occurrence_checklist(PDO $pdo, int $occurrenceId, bool $includeArchived = false): array
|
||||
{
|
||||
market_management_checklist_assert_occurrence($pdo, $occurrenceId, $includeArchived);
|
||||
$parent = $pdo->prepare(
|
||||
'SELECT id, occurrence_id, comment, created_at, updated_at
|
||||
FROM market_occurrence_checklist
|
||||
WHERE occurrence_id = :occurrence_id'
|
||||
);
|
||||
$parent->execute([':occurrence_id' => $occurrenceId]);
|
||||
$checklist = $parent->fetch();
|
||||
if (!is_array($checklist)) {
|
||||
throw new RuntimeException('Checkliste der Marktdurchführung fehlt.');
|
||||
}
|
||||
$items = $pdo->prepare(
|
||||
'SELECT id, occurrence_id, chapter_key, position, label, is_completed, created_at, updated_at
|
||||
FROM market_occurrence_checklist_item
|
||||
WHERE occurrence_id = :occurrence_id
|
||||
ORDER BY id ASC'
|
||||
);
|
||||
$items->execute([':occurrence_id' => $occurrenceId]);
|
||||
$checklist['items'] = $items->fetchAll();
|
||||
$checklist['chapters'] = array_map(
|
||||
static fn (array $chapter): array => ['key' => $chapter['key'], 'label' => $chapter['label']],
|
||||
MARKET_OCCURRENCE_CHECKLIST_CHAPTERS
|
||||
);
|
||||
return $checklist;
|
||||
}
|
||||
|
||||
function market_management_update_occurrence_checklist(PDO $pdo, int $occurrenceId, array $input): array
|
||||
{
|
||||
market_management_checklist_assert_occurrence($pdo, $occurrenceId, true);
|
||||
$items = $input['items'] ?? null;
|
||||
if (!is_array($items)) {
|
||||
throw new InvalidArgumentException('items muss eine Liste sein.');
|
||||
}
|
||||
$comment = $input['comment'] ?? null;
|
||||
if ($comment !== null && !is_string($comment)) {
|
||||
throw new InvalidArgumentException('comment muss ein String oder null sein.');
|
||||
}
|
||||
|
||||
$startedTransaction = !$pdo->inTransaction();
|
||||
if ($startedTransaction) {
|
||||
$pdo->beginTransaction();
|
||||
}
|
||||
try {
|
||||
$parent = $pdo->prepare(
|
||||
'INSERT INTO market_occurrence_checklist (occurrence_id, comment)
|
||||
VALUES (:occurrence_id, :comment)
|
||||
ON CONFLICT (occurrence_id) DO UPDATE
|
||||
SET comment = EXCLUDED.comment, updated_at = NOW()'
|
||||
);
|
||||
$parent->execute([':occurrence_id' => $occurrenceId, ':comment' => $comment === null ? null : trim($comment)]);
|
||||
|
||||
$delete = $pdo->prepare('DELETE FROM market_occurrence_checklist_item WHERE occurrence_id = :occurrence_id');
|
||||
$delete->execute([':occurrence_id' => $occurrenceId]);
|
||||
$insert = $pdo->prepare(
|
||||
'INSERT INTO market_occurrence_checklist_item
|
||||
(occurrence_id, chapter_key, position, label, is_completed)
|
||||
VALUES (:occurrence_id, :chapter_key, :position, :label, :is_completed)'
|
||||
);
|
||||
$chapterKeys = array_fill_keys(array_column(MARKET_OCCURRENCE_CHECKLIST_CHAPTERS, 'key'), true);
|
||||
$chapterPositions = [];
|
||||
foreach (array_values($items) as $position => $item) {
|
||||
if (!is_array($item)) {
|
||||
throw new InvalidArgumentException('Jeder Checklistenpunkt muss ein Objekt sein.');
|
||||
}
|
||||
$chapterKey = $item['chapter_key'] ?? null;
|
||||
if (!is_string($chapterKey) || !isset($chapterKeys[$chapterKey])) {
|
||||
throw new InvalidArgumentException('chapter_key verweist nicht auf ein festes Checklisten-Kapitel.');
|
||||
}
|
||||
$label = $item['label'] ?? '';
|
||||
if (!is_string($label)) {
|
||||
throw new InvalidArgumentException('Der Text eines Checklistenpunkts muss ein String sein.');
|
||||
}
|
||||
$completed = $item['is_completed'] ?? false;
|
||||
if (!is_bool($completed)) {
|
||||
throw new InvalidArgumentException('is_completed muss boolean sein.');
|
||||
}
|
||||
$insert->bindValue(':occurrence_id', $occurrenceId, PDO::PARAM_INT);
|
||||
$chapterPositions[$chapterKey] = ($chapterPositions[$chapterKey] ?? 0) + 1;
|
||||
$insert->bindValue(':chapter_key', $chapterKey, PDO::PARAM_STR);
|
||||
$insert->bindValue(':position', $chapterPositions[$chapterKey], PDO::PARAM_INT);
|
||||
$insert->bindValue(':label', trim($label), PDO::PARAM_STR);
|
||||
$insert->bindValue(':is_completed', $completed, PDO::PARAM_BOOL);
|
||||
$insert->execute();
|
||||
}
|
||||
if ($startedTransaction) {
|
||||
$pdo->commit();
|
||||
}
|
||||
} catch (Throwable $error) {
|
||||
if ($startedTransaction && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $error;
|
||||
}
|
||||
return market_management_get_occurrence_checklist($pdo, $occurrenceId, true);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+585
-83
@@ -50,6 +50,216 @@ function auth_format_bestellung_total(?float $value): string
|
||||
return 'CHF ' . number_format($value, 2, '.', '\'');
|
||||
}
|
||||
|
||||
function auth_format_market_date(?string $earliest, ?string $latest): string
|
||||
{
|
||||
if ($earliest === null || $earliest === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$start = (new DateTimeImmutable($earliest))->format('d.m.Y');
|
||||
if ($latest === null || $latest === '' || $latest === $earliest) {
|
||||
return $start;
|
||||
}
|
||||
|
||||
return $start . '–' . (new DateTimeImmutable($latest))->format('d.m.Y');
|
||||
}
|
||||
|
||||
function auth_market_status_label(string $status): string
|
||||
{
|
||||
return [
|
||||
'open' => 'Offen',
|
||||
'waitlist' => 'Warteliste',
|
||||
'registered' => 'Angemeldet',
|
||||
'confirmation_pending' => 'Bestätigung ausstehend',
|
||||
'confirmed' => 'Bestätigt',
|
||||
'cancelled' => 'Abgesagt',
|
||||
'completed' => 'Abgeschlossen',
|
||||
][$status] ?? $status;
|
||||
}
|
||||
|
||||
function auth_market_status_signal_class(string $status): string
|
||||
{
|
||||
return [
|
||||
'waitlist' => 'market-status--red',
|
||||
'registered' => 'market-status--yellow',
|
||||
'confirmation_pending' => 'market-status--yellow',
|
||||
'confirmed' => 'market-status--green',
|
||||
][$status] ?? '';
|
||||
}
|
||||
|
||||
function auth_large_table_default_width(int $columnCount): string
|
||||
{
|
||||
return rtrim(rtrim(number_format(100 / max(1, $columnCount), 4, '.', ''), '0'), '.') . '%';
|
||||
}
|
||||
|
||||
function auth_large_table_resize_handle(string $label, int $columnIndex, int $columnCount): string
|
||||
{
|
||||
if ($columnIndex >= $columnCount - 1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '<span class="sg-large-table__resize-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-label="Breite von ' . auth_escape_html($label) . ' anpassen" data-large-table-resize-handle="' . $columnIndex . '"></span>';
|
||||
}
|
||||
|
||||
function auth_render_markets_large_table(array $marketTable): string
|
||||
{
|
||||
$rows = $marketTable['rows'] ?? $marketTable;
|
||||
$search = (string) ($marketTable['search'] ?? '');
|
||||
$sortColumn = (string) ($marketTable['sort_column'] ?? 'earliest_date');
|
||||
$sortDirection = strtoupper((string) ($marketTable['sort_direction'] ?? 'ASC'));
|
||||
$limit = (int) ($marketTable['limit'] ?? 20);
|
||||
$pageSize = (int) ($marketTable['page_size'] ?? 20);
|
||||
$hasMore = (bool) ($marketTable['has_more'] ?? (count($rows) >= $limit));
|
||||
$sortColumns = [
|
||||
'market_name' => 'Markt',
|
||||
'earliest_date' => 'Markttag(e)',
|
||||
'place' => 'Ort',
|
||||
'registration_deadline' => 'Anmeldefrist',
|
||||
'status' => 'Status',
|
||||
];
|
||||
|
||||
$html = [];
|
||||
$html[] = '<section class="sg-card-list-page" aria-label="Marktdurchführungen">';
|
||||
$html[] = '<article class="sg-card" data-component="basic-card"><div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-component-part="card-body"><div class="sg-component-row sg-basic-card__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" aria-haspopup="dialog" aria-expanded="false" data-component="button" data-component-state="active" data-market-occurrence-create>Neue Marktdurchführung</button></div></div></article>';
|
||||
$html[] = '<article class="sg-card sg-large-table sg-large-table--markets" data-component="large-table" data-column-resize="true" data-column-resize-key="market-occurrences" data-column-min-width="8rem" data-markets-large-table="true" data-markets-search="' . auth_escape_html($search) . '" data-markets-sort-column="' . auth_escape_html($sortColumn) . '" data-markets-sort-direction="' . auth_escape_html($sortDirection) . '" data-markets-limit="' . $limit . '" data-markets-page-size="' . $pageSize . '" role="table" aria-label="Marktdurchführungen">';
|
||||
$html[] = '<div class="sg-large-table__viewport" data-component-part="large-table-viewport">';
|
||||
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header">';
|
||||
$html[] = '<div class="sg-strong">Marktdurchführungen</div>';
|
||||
$html[] = '<span class="sg-search-field-row" data-component="search-field">';
|
||||
$html[] = '<span class="sg-input-single-line-wrap" data-has-value="' . ($search !== '' ? 'true' : 'false') . '" data-component="single-line-input" data-component-state="' . ($search !== '' ? 'active' : 'inactive-selectable') . '">';
|
||||
$html[] = '<input class="sg-interaction-element sg-input-single-line sg-search-field-input sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" placeholder="Suche" aria-label="Marktdurchführungen durchsuchen" value="' . auth_escape_html($search) . '" data-markets-search-input>';
|
||||
$html[] = '<button class="sg-input-clear-button" type="button" aria-label="Eingabe löschen" data-markets-search-clear>×</button>';
|
||||
$html[] = '</span></span></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">';
|
||||
$headerIndex = 0;
|
||||
foreach ($sortColumns as $column => $label) {
|
||||
$isActive = $column === $sortColumn;
|
||||
$headerDirection = $isActive ? strtolower($sortDirection) : 'none';
|
||||
$iconDirection = $isActive ? strtolower($sortDirection) : 'ascending';
|
||||
$targetDirection = $isActive ? ($sortDirection === 'ASC' ? 'DESC' : 'ASC') : ($column === 'earliest_date' ? 'ASC' : 'ASC');
|
||||
$ariaLabel = $isActive ? $label . ' ' . ($sortDirection === 'ASC' ? 'aufsteigend sortiert' : 'absteigend sortiert') : $label . ' sortieren';
|
||||
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="' . $headerDirection . '" data-sort-key="' . $headerIndex . '" data-default-width="' . auth_large_table_default_width(6) . '"><button class="sg-large-table__sort-button" type="button" aria-label="' . auth_escape_html($ariaLabel) . '" data-markets-sort-button="true" data-markets-sort-column="' . auth_escape_html($column) . '" data-markets-sort-direction="' . auth_escape_html($targetDirection) . '"><span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span><span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="' . auth_escape_html($iconDirection) . '"></span></button>' . auth_large_table_resize_handle($label, $headerIndex, 6) . '</div>';
|
||||
$headerIndex++;
|
||||
}
|
||||
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" data-default-width="' . auth_large_table_default_width(6) . '"><span class="sg-large-table__sort-label">Verkaufte Produkte</span></div>';
|
||||
$html[] = '</div>';
|
||||
|
||||
foreach ($rows as $index => $row) {
|
||||
$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';
|
||||
}
|
||||
$html[] = '<div class="' . $rowClass . '" role="row" data-component-part="large-table-row">';
|
||||
$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>';
|
||||
}
|
||||
|
||||
if ($hasMore) {
|
||||
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--load-more" role="row" data-component-part="large-table-row"><div class="sg-large-table__cell sg-large-table__cell--load-more" role="cell"><article class="sg-card sg-large-table__load-more-card" data-component="card" data-pattern="navigation-card" aria-label="Mehr laden"><div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment"><div class="sg-navigation-card-center"><a class="sg-hyperlink" href="#" data-component="hyperlink" data-markets-load-more-trigger="true">Mehr laden</a></div></div></article></div></div>';
|
||||
}
|
||||
$html[] = '</div></article></section>';
|
||||
return implode('', $html);
|
||||
}
|
||||
|
||||
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)
|
||||
. '<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__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'] ?? [];
|
||||
$sortColumn = (string) ($marketTable['sort_column'] ?? 'name');
|
||||
$sortDirection = strtoupper((string) ($marketTable['sort_direction'] ?? 'ASC')) === 'DESC' ? 'DESC' : 'ASC';
|
||||
$html = [];
|
||||
$html[] = '<div data-market-definitions-content="true" data-market-definitions-csrf-token="' . auth_escape_html(auth_csrf_token()) . '"><section class="sg-card-list-page" aria-label="Märkte">';
|
||||
$html[] = '<article class="sg-card" data-component="basic-card"><div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-component-part="card-body"><div class="sg-component-row sg-basic-card__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" aria-haspopup="dialog" aria-expanded="false" data-component="button" data-market-definition-create>Neuer Markt</button></div></div></article>';
|
||||
$html[] = '<article class="sg-card sg-large-table sg-large-table--markets" data-component="large-table" data-column-resize="true" data-column-resize-key="market-definitions" data-column-min-width="8rem" data-market-definitions-table="true" role="table" aria-label="Märkte">';
|
||||
$html[] = '<div class="sg-large-table__viewport" data-component-part="large-table-viewport">';
|
||||
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header"><div class="sg-strong">Märkte</div></div>';
|
||||
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--header" role="row" data-component-part="large-table-header-row">';
|
||||
$definitionColumns = [
|
||||
'name' => 'Markt',
|
||||
'category_name' => 'Kategorie',
|
||||
'organizer_name' => 'Veranstalter',
|
||||
'website_url' => 'Website',
|
||||
'occurrence_count' => 'Durchführungen',
|
||||
'status' => 'Status',
|
||||
];
|
||||
$definitionColumnIndex = 0;
|
||||
foreach ($definitionColumns as $column => $label) {
|
||||
$isActive = $column === $sortColumn;
|
||||
$headerDirection = $isActive ? strtolower($sortDirection) : 'none';
|
||||
$iconDirection = $isActive && $sortDirection === 'DESC' ? 'descending' : 'ascending';
|
||||
$targetDirection = $isActive && $sortDirection === 'ASC' ? 'DESC' : 'ASC';
|
||||
$ariaLabel = $isActive
|
||||
? $label . ' ' . ($sortDirection === 'ASC' ? 'aufsteigend sortiert' : 'absteigend sortiert')
|
||||
: $label . ' sortieren';
|
||||
$sortButton = '<button class="sg-large-table__sort-button" type="button" aria-label="' . auth_escape_html($ariaLabel) . '" data-market-definition-sort data-market-definition-sort-column="' . auth_escape_html($column) . '" data-market-definition-sort-direction="' . auth_escape_html($targetDirection) . '"><span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span><span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="' . auth_escape_html($iconDirection) . '"></span></button>';
|
||||
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" aria-sort="' . auth_escape_html($headerDirection) . '" data-sort-key="' . $definitionColumnIndex . '" data-default-width="' . auth_large_table_default_width(count($definitionColumns)) . '">' . $sortButton . auth_large_table_resize_handle($label, $definitionColumnIndex, count($definitionColumns)) . '</div>';
|
||||
$definitionColumnIndex++;
|
||||
}
|
||||
$html[] = '</div>';
|
||||
foreach ($rows as $index => $row) {
|
||||
$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[] = '</div></article></section>';
|
||||
$html[] = '<div class="sg-card-list-page-drawer-backdrop" aria-hidden="true" data-market-definition-drawer-backdrop="true" data-open="false"></div>';
|
||||
$html[] = '<aside class="sg-card-list-page-drawer" aria-label="Markt-Details" aria-hidden="true" data-market-definition-drawer="true" data-open="false"><div class="sg-card-list-page-drawer__content" data-market-definition-drawer-content><article class="sg-card sg-card--content-card" data-component="content-card" data-shared-editable-action-card data-market-definition-card><header class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen sg-object-card__header"><h2 class="sg-strong sg-text-on-dark" data-market-definition-read-title>Markt</h2><div class="sg-component-row"><button class="sg-interaction-element sg-button sg-button--active" type="button" data-market-definition-edit data-shared-editable-card-edit>Bearbeiten</button></div></header><div class="sg-card-segment sg-card-segment--body sg-card-segment--white" data-market-definition-read-card data-shared-editable-card-read><dl class="market-detail-grid"><div class="market-detail-row"><dt>Marktname</dt><dd data-market-definition-read-name></dd></div><div class="market-detail-row"><dt>Kategorie/Marktart</dt><dd data-market-definition-read-category></dd></div><div class="market-detail-row"><dt>Veranstalter</dt><dd data-market-definition-read-organizer></dd></div><div class="market-detail-row"><dt>Website</dt><dd data-market-definition-read-website></dd></div></dl></div><div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-market-definition-create-card data-shared-editable-card-form hidden><h2 class="sg-strong" data-market-definition-form-title>Markt bearbeiten</h2><div class="sg-form-sections-card-wrapper" data-pattern="form-sections"><form class="sg-form-sections-card" data-market-definition-form><div class="sg-form-sections-card__body" data-pattern-part="form-body"><div class="sg-form-sections-card__section"><div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Marktname<span class="market-required-field-indicator" aria-hidden="true">*</span></span><input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" name="name" required data-market-definition-name></label></div><div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Kategorie/Marktart<span class="market-required-field-indicator" aria-hidden="true">*</span></span><select class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable sg-pulldown" name="category_id" required data-market-definition-category><option value="">Bitte auswählen</option>';
|
||||
foreach ($categories as $category) {
|
||||
$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'] ?? [];
|
||||
@@ -70,7 +280,8 @@ function auth_render_bestellungen_large_table(array $bestellungenTable): string
|
||||
|
||||
$html = [];
|
||||
$html[] = '<section class="sg-card-list-page" aria-label="Bestellungen">';
|
||||
$html[] = '<article class="sg-card sg-large-table sg-large-table--bestellungen" style="margin-top: var(--spacing-large);" data-component="large-table" data-bestellungen-large-table="true" data-bestellungen-search="' . auth_escape_html($search) . '" data-bestellungen-sort-column="' . auth_escape_html($sortColumn) . '" data-bestellungen-sort-direction="' . auth_escape_html($sortDirection) . '" data-bestellungen-limit="' . $limit . '" data-bestellungen-page-size="' . $pageSize . '" role="table" aria-label="Bestellungen">';
|
||||
$html[] = '<article class="sg-card sg-large-table sg-large-table--bestellungen" style="margin-top: var(--spacing-large);" data-component="large-table" data-column-resize="true" data-column-resize-key="orders" data-column-min-width="8rem" data-bestellungen-large-table="true" data-bestellungen-search="' . auth_escape_html($search) . '" data-bestellungen-sort-column="' . auth_escape_html($sortColumn) . '" data-bestellungen-sort-direction="' . auth_escape_html($sortDirection) . '" data-bestellungen-limit="' . $limit . '" data-bestellungen-page-size="' . $pageSize . '" role="table" aria-label="Bestellungen">';
|
||||
$html[] = '<div class="sg-large-table__viewport" data-component-part="large-table-viewport">';
|
||||
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header">';
|
||||
$html[] = '<div class="sg-strong">Bestellungen</div>';
|
||||
$html[] = '<span class="sg-search-field-row" data-component="search-field">';
|
||||
@@ -95,11 +306,11 @@ function auth_render_bestellungen_large_table(array $bestellungenTable): string
|
||||
: ($column === 'order_date' ? 'DESC' : 'ASC');
|
||||
|
||||
$amountHeaderClass = $column === 'total_amount' ? ' sg-large-table__cell--amount' : '';
|
||||
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header' . $amountHeaderClass . '" role="columnheader" aria-sort="' . $headerDirection . '" data-sort-key="' . $headerIndex . '">';
|
||||
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header' . $amountHeaderClass . '" role="columnheader" aria-sort="' . $headerDirection . '" data-sort-key="' . $headerIndex . '" data-default-width="' . auth_large_table_default_width(4) . '">';
|
||||
$html[] = '<button class="sg-large-table__sort-button" type="button" aria-label="' . auth_escape_html($ariaLabel) . '" data-bestellungen-sort-button="true" data-bestellungen-sort-column="' . auth_escape_html($column) . '" data-bestellungen-sort-direction="' . auth_escape_html($targetDirection) . '">';
|
||||
$html[] = '<span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span>';
|
||||
$html[] = '<span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="' . auth_escape_html($iconDirection) . '"></span>';
|
||||
$html[] = '</button>';
|
||||
$html[] = '</button>' . auth_large_table_resize_handle($label, $headerIndex, 4);
|
||||
$html[] = '</div>';
|
||||
$headerIndex++;
|
||||
}
|
||||
@@ -147,9 +358,7 @@ function auth_render_bestellungen_large_table(array $bestellungenTable): string
|
||||
|
||||
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--load-more" role="row" data-component-part="large-table-row" data-large-table-load-more-row="true">';
|
||||
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--load-more" role="cell">';
|
||||
$html[] = '<div class="sg-navigation-card-layout sg-large-table__load-more-layout">';
|
||||
$html[] = '<div class="sg-navigation-card-block">';
|
||||
$html[] = '<article class="sg-card" data-component="card" data-pattern="navigation-card" aria-label="Mehr laden">';
|
||||
$html[] = '<article class="sg-card sg-large-table__load-more-card" data-component="card" data-pattern="navigation-card" aria-label="Mehr laden">';
|
||||
$html[] = '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment">';
|
||||
$html[] = '<div class="sg-navigation-card-center">';
|
||||
$html[] = '<a class="sg-hyperlink" href="' . auth_escape_html($loadMoreUrl) . '" data-component="hyperlink" data-large-table-load-more-trigger="true">Mehr laden</a>';
|
||||
@@ -157,11 +366,9 @@ function auth_render_bestellungen_large_table(array $bestellungenTable): string
|
||||
$html[] = '</div>';
|
||||
$html[] = '</article>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
}
|
||||
|
||||
$html[] = '</div>';
|
||||
$html[] = '</article>';
|
||||
$html[] = '</section>';
|
||||
|
||||
@@ -214,11 +421,12 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
|
||||
return ['text' => $text, 'warning' => $warning, 'mhdWarning' => $mhdWarning];
|
||||
};
|
||||
$html = [];
|
||||
$html[] = '<article class="sg-card sg-large-table sg-large-table--lager" data-component="large-table" data-lager-table="true" data-lager-table-key="' . auth_escape_html($tableKey) . '" role="table" aria-label="' . auth_escape_html($title) . '">';
|
||||
$html[] = '<article class="sg-card sg-large-table sg-large-table--lager" data-component="large-table" data-column-resize="true" data-column-resize-key="lager-' . auth_escape_html($tableKey) . '" data-column-min-width="8rem" data-lager-table="true" data-lager-table-key="' . auth_escape_html($tableKey) . '" role="table" aria-label="' . auth_escape_html($title) . '">';
|
||||
$html[] = '<div class="sg-large-table__viewport" data-component-part="large-table-viewport">';
|
||||
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header"><div class="sg-strong">' . auth_escape_html($title) . '</div></div>';
|
||||
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--header" role="row" data-component-part="large-table-header-row">';
|
||||
foreach ($columns as $label) {
|
||||
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader"><span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span></div>';
|
||||
foreach ($columns as $index => $label) {
|
||||
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader" data-default-width="' . auth_large_table_default_width(count($columns)) . '"><span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span>' . auth_large_table_resize_handle($label, $index, count($columns)) . '</div>';
|
||||
}
|
||||
$html[] = '</div>';
|
||||
|
||||
@@ -282,7 +490,10 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
|
||||
|
||||
$html[] = '<div class="' . $rowClass . '" role="row" data-component-part="large-table-row" data-lager-row="true" data-lager-id="' . $id . '">';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell"><a class="sg-hyperlink" href="#" aria-haspopup="dialog" aria-expanded="false" data-lager-drawer-open="true"' . $nameData . $valueData . $typeData . $detailData . '>' . auth_escape_html($name) . '</a></div>';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell"><a class="sg-hyperlink" href="#" aria-haspopup="dialog" aria-expanded="false" data-lager-drawer-open="true"' . $nameData . $valueData . $typeData . $detailData . '>' . auth_escape_html($value) . '</a></div>';
|
||||
$valueCell = $tableKey === 'products'
|
||||
? '<div class="sg-large-table__cell" role="cell">' . auth_escape_html($value) . '</div>'
|
||||
: '<div class="sg-large-table__cell" role="cell"><a class="sg-hyperlink" href="#" aria-haspopup="dialog" aria-expanded="false" data-lager-drawer-open="true"' . $nameData . $valueData . $typeData . $detailData . '>' . auth_escape_html($value) . '</a></div>';
|
||||
$html[] = $valueCell;
|
||||
if ($tableKey === 'products') {
|
||||
$forecastClass = $currentForecast['warning']
|
||||
? ' sg-strong lager-forecast-warning'
|
||||
@@ -297,6 +508,7 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
|
||||
$html[] = '</div>';
|
||||
}
|
||||
|
||||
$html[] = '</div>';
|
||||
$html[] = '</article>';
|
||||
return implode('', $html);
|
||||
}
|
||||
@@ -328,17 +540,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>';
|
||||
}
|
||||
@@ -364,18 +576,11 @@ function auth_render_lager_open_lots(array $lagerProducts, string $manufacturerO
|
||||
&& trim((string) ($lot['purchase_date'] ?? '')) !== ''
|
||||
&& (float) ($lot['qty_net'] ?? 0) > 0;
|
||||
$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" 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>Löschen</button><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-open-edit-open>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) . '"><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 sg-card-segment--gray" data-lager-open-edit-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>'
|
||||
$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><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>'
|
||||
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Hersteller</span><select class="sg-interaction-element sg-input-single-line" data-lager-open-manufacturer>' . str_replace('value="' . (int) ($lot['manufacturer_party_id'] ?? 0) . '"', 'value="' . (int) ($lot['manufacturer_party_id'] ?? 0) . '" selected', $manufacturerOptions) . '</select></label></div>'
|
||||
@@ -384,9 +589,9 @@ function auth_render_lager_open_lots(array $lagerProducts, string $manufacturerO
|
||||
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">MHD</span><input class="sg-interaction-element sg-input-single-line" type="date" data-lager-open-expiry-date value="' . auth_escape_html((string) ($lot['expiry_date'] ?? '')) . '"></label></div>'
|
||||
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Einkaufsdatum</span><input class="sg-interaction-element sg-input-single-line" type="date" data-lager-open-purchase-date value="' . auth_escape_html((string) ($lot['purchase_date'] ?? '')) . '"></label></div>'
|
||||
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Menge</span><input class="sg-interaction-element sg-input-single-line" type="number" min="0" step="1" inputmode="numeric" data-lager-open-target-qty value="' . auth_escape_html((string) ($lot['qty_net'] ?? 0)) . '"></label></div>'
|
||||
. '</section><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-lager-open-edit-close>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-lager-open-edit-status aria-live="polite"></p></div></form></div></div></article>'
|
||||
. '</section><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-lager-open-edit-close 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-lager-open-edit-status aria-live="polite"></p></div></form></div></div></article>'
|
||||
. '<article class="sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card" aria-label="Leere Felder bestätigen" role="dialog" aria-modal="true" aria-labelledby="lager-open-confirmation-title-' . $lotId . '" data-lager-open-confirmation-card 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="lager-open-confirmation-title-' . $lotId . '"><strong>Leere Felder speichern?</strong></p><p class="sg-body sg-delete-confirmation-pattern__text">Die folgenden Felder werden leer gespeichert:</p><div class="sg-delete-confirmation-pattern__list" data-lager-open-confirmation-empty-fields aria-label="Leer zu speichernde Felder"></div><div class="sg-delete-confirmation-pattern__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-open-confirmation-cancel>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process" type="button" data-lager-open-confirmation-submit>Speichern</button></div></div></article>'
|
||||
. '<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="lager-open-delete-title-' . $lotId . '" data-overlay-dialog="delete" data-lager-open-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="lager-open-delete-title-' . $lotId . '"><strong>Möchtest du diese offene Charge 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-lager-open-delete-confirmation-input></label><p class="sg-body" data-lager-open-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-lager-open-delete-confirmation-cancel>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process sg-button--process-inactive" type="button" disabled aria-disabled="true" data-lager-open-delete-confirmation-submit>Löschen</button></div></div></article></div>';
|
||||
. '<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="lager-open-delete-title-' . $lotId . '" data-overlay-dialog="delete" data-lager-open-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="lager-open-delete-title-' . $lotId . '"><strong>Möchtest du diese offene Charge 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-lager-open-delete-confirmation-input data-shared-editable-card-delete-input></label><p class="sg-body" data-lager-open-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-lager-open-delete-confirmation-cancel 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-lager-open-delete-confirmation-submit data-shared-editable-card-delete-submit>Löschen</button></div></div></article></div>';
|
||||
}
|
||||
$html[] = '</div></div>';
|
||||
}
|
||||
@@ -454,10 +659,10 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
|
||||
. '</article>'
|
||||
. '<template data-lager-open-lot-template>' . $openLotCardTemplate . '</template>'
|
||||
. '<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" 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>Bearbeiten</button></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-lager-read-view><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 sg-card-segment--gray" data-lager-edit-form hidden><div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Aktive Charge bearbeiten">'
|
||||
. '<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><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>'
|
||||
. '<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 sg-form-inactive-selectable" type="text" data-lager-lot-number readonly></label></div>'
|
||||
@@ -471,7 +676,7 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
|
||||
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Zugang buchen</span><input class="sg-interaction-element sg-input-single-line" type="number" min="0" step="0.01" inputmode="decimal" data-lager-receipt-qty placeholder="Menge"></label></div>'
|
||||
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Bestandskorrektur (+ / −)</span><input class="sg-interaction-element sg-input-single-line" type="number" step="1" inputmode="numeric" data-lager-adjustment-qty placeholder="+ / − Menge"></label></div>'
|
||||
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Abgang buchen</span><input class="sg-interaction-element sg-input-single-line" type="number" min="0" step="0.01" inputmode="decimal" data-lager-out-qty placeholder="Menge"></label></div>'
|
||||
. '</section><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-lager-edit-close>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-lager-edit-status aria-live="polite"></p></div></form>'
|
||||
. '</section><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-lager-edit-close 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-lager-edit-status aria-live="polite"></p></div></form>'
|
||||
. '</div></div>'
|
||||
. '</article>'
|
||||
. '<div data-lager-drawer-topic="historical-lots" aria-label="Vergangene Chargen">' . auth_render_lager_historical_lots($lagerProducts) . '</div>'
|
||||
@@ -484,7 +689,7 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
function render_auth_home_page(array $user, array $otcProducts = [], array $bestellungenTable = [], 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) {
|
||||
@@ -504,6 +709,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>';
|
||||
}
|
||||
@@ -512,16 +737,21 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
[
|
||||
'Übersicht' => [],
|
||||
'ERP' => ['Bestellungen', 'Lager', 'Produkte', 'Artikel', 'Kontakte'],
|
||||
'Märkte' => ['Marktdurchführungen', 'Märkte'],
|
||||
'Buchhaltung' => [],
|
||||
'Kundenberatung' => [],
|
||||
],
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
$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,
|
||||
'Marktdurchführungen' => $marketsContent,
|
||||
'Märkte' => $marketDefinitionsContent,
|
||||
'Lager' => $lagerContent,
|
||||
'Artikel' => auth_render_lager_content([], $lagerArtikel, $lagerManufacturers, auth_csrf_token(), 'Artikel'),
|
||||
],
|
||||
@@ -531,6 +761,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
[
|
||||
'Übersicht' => 'Willkommen bei Naurua Übersicht',
|
||||
'ERP' => 'Willkommen bei Naurua ERP',
|
||||
'Märkte' => 'Willkommen bei Naurua Märkte',
|
||||
'Buchhaltung' => 'Willkommen bei Naurua Buchhaltung',
|
||||
'Kundenberatung' => 'Willkommen bei Naurua Kundenberatung',
|
||||
],
|
||||
@@ -543,7 +774,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo '<meta charset="UTF-8">';
|
||||
echo '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
|
||||
echo '<title>Naurua ERP</title>';
|
||||
echo '<link rel="stylesheet" href="/assets/styles.css">';
|
||||
echo '<link rel="stylesheet" href="/assets/styles.css?v=' . filemtime(__DIR__ . '/../../../../public/assets/styles.css') . '">';
|
||||
echo '<link rel="stylesheet" href="/assets/styles.portal.css?v=' . filemtime(__DIR__ . '/../../../../public/assets/styles.portal.css') . '">';
|
||||
echo '</head>';
|
||||
echo '<body>';
|
||||
@@ -568,6 +799,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo '<nav class="sg-portal-header__tabs sg-tab-button-group" aria-label="Hauptnavigation" data-component="tab-navigation" data-component-size="large" data-component-context="portal-header" data-pattern-part="portal-header-navigation">';
|
||||
echo '<button class="sg-interaction-element sg-button sg-tab-button" type="button" aria-selected="false" data-component-part="tab-button" data-component-state="inactive">Übersicht</button>';
|
||||
echo '<button class="sg-interaction-element sg-button sg-tab-button" type="button" aria-selected="true" data-component-part="tab-button" data-component-state="active">ERP</button>';
|
||||
echo '<button class="sg-interaction-element sg-button sg-tab-button" type="button" aria-selected="false" data-component-part="tab-button" data-component-state="inactive">Märkte</button>';
|
||||
echo '<button class="sg-interaction-element sg-button sg-tab-button" type="button" aria-selected="false" data-component-part="tab-button" data-component-state="inactive">Buchhaltung</button>';
|
||||
echo '<button class="sg-interaction-element sg-button sg-tab-button" type="button" aria-selected="false" data-component-part="tab-button" data-component-state="inactive">Kundenberatung</button>';
|
||||
echo '</nav>';
|
||||
@@ -636,7 +868,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo '</select>';
|
||||
echo '</label>';
|
||||
echo '<label class="sg-labeled-input-row"><span class="sg-label">Betrag (CHF)</span>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="number" id="otc-payment-amount-1" min="0.01" step="0.01" value="0.00" data-otc-payment-amount>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="number" id="otc-payment-amount-1" min="0.01" step="0.01" placeholder="0.00" data-otc-payment-amount>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
@@ -676,6 +908,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>';
|
||||
@@ -697,6 +955,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>';
|
||||
@@ -706,26 +965,36 @@ 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?v=20260930-2"></script>';
|
||||
echo '<script src="/scripts/shared-mobile-detail.js?v=20260925-1"></script>';
|
||||
echo '<script src="/scripts/large-table-column-resize.js?v=' . filemtime(__DIR__ . '/../../../../public/scripts/large-table-column-resize.js') . '"></script>';
|
||||
echo '<script>';
|
||||
echo 'const portalModuleNavigation = ' . $moduleNavigation . ';';
|
||||
echo 'const portalModuleContentCards = ' . $moduleContentCards . ';';
|
||||
echo 'const portalModuleHeadings = ' . $moduleHeadings . ';';
|
||||
echo "const portalContentBody = document.querySelector('[data-left-navigation-content-body]');";
|
||||
echo "if (portalContentBody && window.sgInitLargeTableColumnResize) { const portalLargeTableResizeObserver = new MutationObserver(() => window.sgInitLargeTableColumnResize({ root: portalContentBody })); portalLargeTableResizeObserver.observe(portalContentBody, { childList: true }); window.sgInitLargeTableColumnResize({ root: portalContentBody }); }";
|
||||
echo "const renderMainHeading = (moduleName) => {";
|
||||
echo " const heading = document.querySelector('[data-main-heading]');";
|
||||
echo " if (heading && portalModuleHeadings[moduleName]) { heading.textContent = portalModuleHeadings[moduleName]; }";
|
||||
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 === 'Marktdurchführungen') { initMarketsBindings(); }";
|
||||
echo " if (activeEntry === 'Märkte') { initMarketDefinitionsBindings(); }";
|
||||
echo " if (activeEntry === 'Lager' || activeEntry === 'Artikel') { initLagerBindings(); }";
|
||||
echo " menu.innerHTML = '';";
|
||||
echo " entries.forEach((entry, index) => {";
|
||||
@@ -734,7 +1003,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 " });";
|
||||
@@ -743,11 +1012,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 === '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 "};";
|
||||
@@ -759,8 +1032,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 "});";
|
||||
@@ -831,18 +1106,19 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " if (lagerBindingsInstalled) { return; }";
|
||||
echo " const contentRoot = document.querySelector('[data-left-navigation-content-body]');";
|
||||
echo " if (!contentRoot || !contentRoot.querySelector('[data-lager-content]')) { return; }";
|
||||
echo " if (window.SharedEditableActionCard) { window.SharedEditableActionCard.init(contentRoot); }";
|
||||
echo " lagerBindingsInstalled = true;";
|
||||
echo " let lagerMobileDetailHost = null;";
|
||||
echo " let lagerMobileDetailOpen = false;";
|
||||
echo " const renderLagerMobileDetail = (sourceDrawer, trigger) => { const type = trigger.dataset.lagerDrawerType || 'Lager'; const name = trigger.dataset.lagerDrawerName || ''; const value = trigger.dataset.lagerDrawerValue || ''; sourceDrawer.querySelector('[data-lager-drawer-title]').textContent = type + ': ' + name; sourceDrawer.querySelector('[data-lager-drawer-name-value]').textContent = name; sourceDrawer.querySelector('[data-lager-drawer-value-label]').textContent = type === 'Produkt' ? 'Anzahl im Lager:' : 'Artikelnummer:'; sourceDrawer.querySelector('[data-lager-drawer-value-value]').textContent = value; sourceDrawer.querySelectorAll('[data-lager-drawer-topic]').forEach((card) => { card.hidden = type !== 'Produkt'; }); const newLotForm = sourceDrawer.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); if (newLotForm) { newLotForm.hidden = true; } const newLotAction = sourceDrawer.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); if (newLotAction) { newLotAction.hidden = false; } if (type === 'Produkt') { const warning = trigger.dataset.lagerDrawerForecastWarning === 'true' || trigger.dataset.lagerDrawerForecastMhdWarning === 'true'; const warningValue = sourceDrawer.querySelector('[data-lager-drawer-warning-value]'); if (warningValue) { warningValue.textContent = warning ? (trigger.dataset.lagerDrawerForecast || 'Keine Prognose') : 'Keine akute Warnung.'; } const warningTitle = sourceDrawer.querySelector('[data-lager-drawer-warning-title]'); if (warningTitle) { warningTitle.textContent = getLagerWarningTitle(trigger); } sourceDrawer.querySelector('[data-lager-drawer-lot-number]').textContent = trigger.dataset.lagerDrawerLotNumber || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-current-qty]').textContent = trigger.dataset.lagerDrawerCurrentQty || '0'; sourceDrawer.querySelector('[data-lager-drawer-qty-in]').textContent = trigger.dataset.lagerDrawerQtyIn || '0'; sourceDrawer.querySelector('[data-lager-drawer-manufacturer-name]').textContent = trigger.dataset.lagerDrawerManufacturerName || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-manufacturer-lot-number]').textContent = trigger.dataset.lagerDrawerManufacturerLotNumber || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-mfg-date]').textContent = trigger.dataset.lagerDrawerMfgDate || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-expiry-date]').textContent = trigger.dataset.lagerDrawerExpiryDate || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-purchase-date]').textContent = trigger.dataset.lagerDrawerPurchaseDate || 'Nicht hinterlegt'; const productId = trigger.closest('[data-lager-row]')?.dataset.lagerId || ''; sourceDrawer.querySelectorAll('[data-lager-historical-product]').forEach((product) => { product.hidden = product.dataset.lagerHistoricalProduct !== productId; }); } const host = document.createElement('div'); host.className = 'sg-lager-mobile-detail-host'; host.dataset.lagerMobileDetailHost = 'true'; host.innerHTML = '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\" aria-label=\"Navigation Detailansicht zurück oben\"><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\" data-pattern-part=\"navigation-card-segment\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-lager-detail-back>zurück</button></div></div></article></div></div>'; const mobileDrawer = sourceDrawer; mobileDrawer.dataset.lagerMobileDrawer = 'true'; mobileDrawer.dataset.open = 'true'; mobileDrawer.setAttribute('aria-hidden', 'false'); host.appendChild(sourceDrawer); return host; };";
|
||||
echo " const lagerMobileDetail = window.SharedMobileDetail?.create({ contentRoot, bodyClass: 'sg-lager-mobile-detail-open', hostClass: 'sg-lager-mobile-detail-host', hostDataAttribute: 'data-lager-mobile-detail-host', drawerDataAttribute: 'data-lager-mobile-drawer', backMarkup: '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\" aria-label=\"Navigation Detailansicht zurück oben\"><div class=\"sg-card-segment sg-card-segment--body\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-lager-detail-back>zurück</button></div></div></article></div></div>', historyKey: 'lagerView' });";
|
||||
echo " const renderLagerMobileDetail = (sourceDrawer, trigger) => { const type = trigger.dataset.lagerDrawerType || 'Lager'; const name = trigger.dataset.lagerDrawerName || ''; const value = trigger.dataset.lagerDrawerValue || ''; sourceDrawer.querySelector('[data-lager-drawer-title]').textContent = type + ': ' + name; sourceDrawer.querySelector('[data-lager-drawer-name-value]').textContent = name; sourceDrawer.querySelector('[data-lager-drawer-value-label]').textContent = type === 'Produkt' ? 'Anzahl im Lager:' : 'Artikelnummer:'; sourceDrawer.querySelector('[data-lager-drawer-value-value]').textContent = value; sourceDrawer.querySelectorAll('[data-lager-drawer-topic]').forEach((card) => { card.hidden = type !== 'Produkt'; }); const newLotForm = sourceDrawer.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); if (newLotForm) { newLotForm.hidden = true; } const newLotAction = sourceDrawer.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); if (newLotAction) { newLotAction.hidden = false; } if (type === 'Produkt') { const warning = trigger.dataset.lagerDrawerForecastWarning === 'true' || trigger.dataset.lagerDrawerForecastMhdWarning === 'true'; const warningValue = sourceDrawer.querySelector('[data-lager-drawer-warning-value]'); if (warningValue) { warningValue.textContent = warning ? (trigger.dataset.lagerDrawerForecast || 'Keine Prognose') : 'Keine akute Warnung.'; } const warningTitle = sourceDrawer.querySelector('[data-lager-drawer-warning-title]'); if (warningTitle) { warningTitle.textContent = getLagerWarningTitle(trigger); } sourceDrawer.querySelector('[data-lager-drawer-lot-number]').textContent = trigger.dataset.lagerDrawerLotNumber || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-current-qty]').textContent = trigger.dataset.lagerDrawerCurrentQty || '0'; sourceDrawer.querySelector('[data-lager-drawer-qty-in]').textContent = trigger.dataset.lagerDrawerQtyIn || '0'; sourceDrawer.querySelector('[data-lager-drawer-manufacturer-name]').textContent = trigger.dataset.lagerDrawerManufacturerName || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-manufacturer-lot-number]').textContent = trigger.dataset.lagerDrawerManufacturerLotNumber || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-mfg-date]').textContent = trigger.dataset.lagerDrawerMfgDate || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-expiry-date]').textContent = trigger.dataset.lagerDrawerExpiryDate || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-purchase-date]').textContent = trigger.dataset.lagerDrawerPurchaseDate || 'Nicht hinterlegt'; const productId = trigger.closest('[data-lager-row]')?.dataset.lagerId || ''; sourceDrawer.querySelectorAll('[data-lager-historical-product]').forEach((product) => { product.hidden = product.dataset.lagerHistoricalProduct !== productId; }); sourceDrawer.querySelectorAll('[data-lager-open-product]').forEach((product) => { product.hidden = product.dataset.lagerOpenProduct !== productId; product.querySelectorAll('[data-lager-open-edit-form]').forEach((form) => { form.hidden = true; }); product.querySelectorAll('[data-lager-open-card]').forEach((readView) => { readView.hidden = false; }); }); resetLagerNewLotConfirmation(); resetLagerOpenLotConfirmations(); sourceDrawer.dataset.lagerProductId = productId; populateLagerActiveLotForm(sourceDrawer, trigger); } const host = document.createElement('div'); host.className = 'sg-lager-mobile-detail-host'; host.dataset.lagerMobileDetailHost = 'true'; host.innerHTML = '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\" aria-label=\"Navigation Detailansicht zurück oben\"><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\" data-pattern-part=\"navigation-card-segment\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-lager-detail-back>zurück</button></div></div></article></div></div>'; const mobileDrawer = sourceDrawer; mobileDrawer.dataset.lagerMobileDrawer = 'true'; mobileDrawer.dataset.open = 'true'; mobileDrawer.setAttribute('aria-hidden', 'false'); host.appendChild(sourceDrawer); return host; };";
|
||||
echo " const restoreLagerMobileDetail = () => {";
|
||||
echo " if (lagerMobileDetailHost && lagerMobileDetailHost.parentNode) { const mobileDrawer = lagerMobileDetailHost.querySelector('[data-lager-mobile-drawer]'); if (mobileDrawer) { contentRoot.appendChild(mobileDrawer); mobileDrawer.removeAttribute('data-lager-mobile-drawer'); mobileDrawer.dataset.open = 'false'; mobileDrawer.setAttribute('aria-hidden', 'true'); } lagerMobileDetailHost.parentNode.removeChild(lagerMobileDetailHost); }";
|
||||
echo " lagerMobileDetailHost = null; lagerMobileDetailOpen = false; document.body.classList.remove('sg-lager-mobile-detail-open');";
|
||||
echo " lagerMobileDetail?.restore(); lagerMobileDetailHost = null; lagerMobileDetailOpen = false;";
|
||||
echo " };";
|
||||
echo " const openLagerMobileDetail = (trigger) => {";
|
||||
echo " restoreLagerMobileDetail(); const sourceDrawer = contentRoot.querySelector('[data-lager-drawer]'); lagerMobileDetailHost = sourceDrawer ? renderLagerMobileDetail(sourceDrawer, trigger) : null; const mobileDrawer = lagerMobileDetailHost?.querySelector('[data-lager-mobile-drawer]'); if (mobileDrawer) { const virtualLot = trigger.dataset.lagerDrawerVirtualLotNumber || ''; const virtualLotValue = mobileDrawer.querySelector('[data-lager-drawer-virtual-lot-value]'); if (virtualLotValue) { virtualLotValue.textContent = virtualLot ? 'Virtuelle Charge: ' + virtualLot : 'Keine virtuelle Fehlbestandscharge vorhanden.'; } }";
|
||||
echo " if (lagerMobileDetailHost) { document.body.appendChild(lagerMobileDetailHost); document.body.classList.add('sg-lager-mobile-detail-open'); }";
|
||||
echo " lagerMobileDetailOpen = true; window.history.pushState({ ...(window.history.state || {}), lagerView: 'detail' }, '', window.location.href);";
|
||||
echo " if (lagerMobileDetailHost && mobileDrawer) { lagerMobileDetail?.mount(mobileDrawer, trigger, lagerMobileDetailHost); if (window.SharedEditableActionCard) { window.SharedEditableActionCard.init(lagerMobileDetailHost); } }";
|
||||
echo " lagerMobileDetailOpen = true;";
|
||||
echo " };";
|
||||
echo " const closeLagerMobileDetail = () => {";
|
||||
echo " if (!lagerMobileDetailOpen) { return; }";
|
||||
@@ -850,6 +1126,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " restoreLagerMobileDetail();";
|
||||
echo " };";
|
||||
echo " const getDrawer = () => document.querySelector('[data-lager-mobile-drawer]') || contentRoot.querySelector('[data-lager-drawer]');";
|
||||
echo " const populateLagerActiveLotForm = (drawer, trigger) => { if (!drawer || !trigger) { return; } const values = { '[data-lager-lot-number]': trigger.dataset.lagerDrawerLotNumber || '', '[data-lager-manufacturer]': trigger.dataset.lagerDrawerManufacturerPartyId || '', '[data-lager-manufacturer-lot]': trigger.dataset.lagerDrawerManufacturerLotNumber || '', '[data-lager-mfg-date]': trigger.dataset.lagerDrawerMfgDate || '', '[data-lager-expiry-date]': trigger.dataset.lagerDrawerExpiryDate || '', '[data-lager-purchase-date]': trigger.dataset.lagerDrawerPurchaseDate || '' }; Object.entries(values).forEach(([selector, value]) => { const field = drawer.querySelector(selector); if (field) { field.value = value; } }); drawer.dataset.lagerActiveLotId = trigger.dataset.lagerDrawerLotId || ''; };";
|
||||
echo " document.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open][data-lager-drawer-type=\"Produkt\"]'); if (!trigger) { return; } const drawer = getDrawer(); const lotValue = drawer?.querySelector('[data-lager-drawer-virtual-lot-value]'); const qtyValue = drawer?.querySelector('[data-lager-drawer-virtual-lot-qty]'); if (lotValue) { lotValue.textContent = trigger.dataset.lagerDrawerVirtualLotNumber ? 'Virtuelle Charge: ' + trigger.dataset.lagerDrawerVirtualLotNumber : 'Keine virtuelle Fehlbestandscharge vorhanden.'; } if (qtyValue) { qtyValue.textContent = 'Bereits virtuell verkauft: ' + (trigger.dataset.lagerDrawerVirtualShortageQty || '0'); } });";
|
||||
echo " const setDrawerState = (open) => {";
|
||||
echo " const drawer = getDrawer(); const backdrop = contentRoot.querySelector('[data-lager-drawer-backdrop]');";
|
||||
@@ -865,7 +1142,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " document.addEventListener('lager-save-complete', (event) => { const forecast = event.detail?.forecast; const productId = event.detail?.productId || ''; const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]') || contentRoot.querySelector('[data-lager-row][data-lager-id=\"' + productId + '\"] [data-lager-drawer-open]'); const drawer = getDrawer(); if (!forecast || !drawer) { return; } const date = forecast.date ? new Date(forecast.date + 'T00:00:00').toLocaleDateString('de-CH') : 'Keine Prognose'; const text = forecast.status === 'depleted' ? date + ' (BESTAND ENDE)' : date; const value = drawer.querySelector('[data-lager-drawer-forecast-value]'); if (value) { value.textContent = text; } if (trigger) { trigger.dataset.lagerDrawerForecast = text; const row = trigger.closest('[data-lager-row]'); const cells = row?.querySelectorAll('[role=\"cell\"]'); const forecastCell = cells?.[cells.length - 1]; if (forecastCell && cells.length === 3) { forecastCell.textContent = text; } } });";
|
||||
echo " document.addEventListener('click', (event) => {";
|
||||
echo " const trigger = event.target.closest('[data-lager-drawer-open]');";
|
||||
echo " if (trigger) { event.preventDefault(); if (window.matchMedia('(max-width: 767px)').matches) { openLagerMobileDetail(trigger); return; } const drawer = getDrawer(); if (!drawer) { return; } const type = trigger.dataset.lagerDrawerType || 'Lager'; const name = trigger.dataset.lagerDrawerName || ''; const value = trigger.dataset.lagerDrawerValue || ''; drawer.querySelector('[data-lager-drawer-title]').textContent = type + ': ' + name; drawer.querySelector('[data-lager-drawer-name-value]').textContent = name; const valueRow = drawer.querySelector('[data-lager-drawer-value-row]'); if (valueRow) { valueRow.hidden = false; } drawer.querySelector('[data-lager-drawer-value-label]').textContent = type === 'Produkt' ? 'Anzahl im Lager:' : 'Artikelnummer:'; drawer.querySelector('[data-lager-drawer-value-value]').textContent = value; drawer.querySelectorAll('[data-lager-drawer-topic]').forEach((card) => { card.hidden = type !== 'Produkt'; }); if (type === 'Produkt') { const forecast = trigger.dataset.lagerDrawerForecast || 'Keine Prognose'; const warning = trigger.dataset.lagerDrawerForecastWarning === 'true' || trigger.dataset.lagerDrawerForecastMhdWarning === 'true'; const warningValue = drawer.querySelector('[data-lager-drawer-warning-value]'); if (warningValue) { warningValue.textContent = warning ? forecast : 'Keine akute Warnung.'; warningValue.classList.toggle('sg-strong', warning); warningValue.classList.toggle('lager-forecast-warning', trigger.dataset.lagerDrawerForecastWarning === 'true'); warningValue.classList.toggle('lager-forecast-mhd-warning', trigger.dataset.lagerDrawerForecastMhdWarning === 'true'); } drawer.querySelector('[data-lager-drawer-lot-number]').textContent = trigger.dataset.lagerDrawerLotNumber || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-manufacturer-name]').textContent = trigger.dataset.lagerDrawerManufacturerName || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-manufacturer-lot-number]').textContent = trigger.dataset.lagerDrawerManufacturerLotNumber || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-mfg-date]').textContent = trigger.dataset.lagerDrawerMfgDate || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-expiry-date]').textContent = trigger.dataset.lagerDrawerExpiryDate || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-purchase-date]').textContent = trigger.dataset.lagerDrawerPurchaseDate || 'Nicht hinterlegt'; const virtualLot = trigger.dataset.lagerDrawerVirtualLotNumber || ''; drawer.querySelector('[data-lager-drawer-virtual-lot-value]').textContent = virtualLot ? 'Virtuelle Charge: ' + virtualLot : 'Keine virtuelle Fehlbestandscharge vorhanden.'; const manufacturer = drawer.querySelector('[data-lager-manufacturer]'); if (manufacturer) { manufacturer.value = trigger.dataset.lagerDrawerManufacturerPartyId || ''; } const manufacturerLot = drawer.querySelector('[data-lager-manufacturer-lot]'); if (manufacturerLot) { manufacturerLot.value = trigger.dataset.lagerDrawerManufacturerLotNumber || ''; } const mfgDate = drawer.querySelector('[data-lager-mfg-date]'); if (mfgDate) { mfgDate.value = trigger.dataset.lagerDrawerMfgDate || ''; } const expiryDate = drawer.querySelector('[data-lager-expiry-date]'); if (expiryDate) { expiryDate.value = trigger.dataset.lagerDrawerExpiryDate || ''; } const purchaseDate = drawer.querySelector('[data-lager-purchase-date]'); if (purchaseDate) { purchaseDate.value = trigger.dataset.lagerDrawerPurchaseDate || ''; } const editForm = drawer.querySelector('[data-lager-edit-form]'); if (editForm) { editForm.hidden = true; } drawer.dataset.lagerActiveLotId = trigger.dataset.lagerDrawerLotId || ''; } setDrawerState(true); contentRoot.querySelectorAll('[data-lager-drawer-open]').forEach((button) => button.setAttribute('aria-expanded', String(button === trigger))); return; }";
|
||||
echo " if (trigger) { event.preventDefault(); if (window.matchMedia('(max-width: 767px)').matches) { openLagerMobileDetail(trigger); return; } const drawer = getDrawer(); if (!drawer) { return; } const type = trigger.dataset.lagerDrawerType || 'Lager'; const name = trigger.dataset.lagerDrawerName || ''; const value = trigger.dataset.lagerDrawerValue || ''; drawer.querySelector('[data-lager-drawer-title]').textContent = type + ': ' + name; drawer.querySelector('[data-lager-drawer-name-value]').textContent = name; const valueRow = drawer.querySelector('[data-lager-drawer-value-row]'); if (valueRow) { valueRow.hidden = false; } drawer.querySelector('[data-lager-drawer-value-label]').textContent = type === 'Produkt' ? 'Anzahl im Lager:' : 'Artikelnummer:'; drawer.querySelector('[data-lager-drawer-value-value]').textContent = value; drawer.querySelectorAll('[data-lager-drawer-topic]').forEach((card) => { card.hidden = type !== 'Produkt'; }); if (type === 'Produkt') { const forecast = trigger.dataset.lagerDrawerForecast || 'Keine Prognose'; const warning = trigger.dataset.lagerDrawerForecastWarning === 'true' || trigger.dataset.lagerDrawerForecastMhdWarning === 'true'; const warningValue = drawer.querySelector('[data-lager-drawer-warning-value]'); if (warningValue) { warningValue.textContent = warning ? forecast : 'Keine akute Warnung.'; warningValue.classList.toggle('sg-strong', warning); warningValue.classList.toggle('lager-forecast-warning', trigger.dataset.lagerDrawerForecastWarning === 'true'); warningValue.classList.toggle('lager-forecast-mhd-warning', trigger.dataset.lagerDrawerForecastMhdWarning === 'true'); } drawer.querySelector('[data-lager-drawer-lot-number]').textContent = trigger.dataset.lagerDrawerLotNumber || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-manufacturer-name]').textContent = trigger.dataset.lagerDrawerManufacturerName || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-manufacturer-lot-number]').textContent = trigger.dataset.lagerDrawerManufacturerLotNumber || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-mfg-date]').textContent = trigger.dataset.lagerDrawerMfgDate || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-expiry-date]').textContent = trigger.dataset.lagerDrawerExpiryDate || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-purchase-date]').textContent = trigger.dataset.lagerDrawerPurchaseDate || 'Nicht hinterlegt'; const virtualLot = trigger.dataset.lagerDrawerVirtualLotNumber || ''; drawer.querySelector('[data-lager-drawer-virtual-lot-value]').textContent = virtualLot ? 'Virtuelle Charge: ' + virtualLot : 'Keine virtuelle Fehlbestandscharge vorhanden.'; populateLagerActiveLotForm(drawer, trigger); const editForm = drawer.querySelector('[data-lager-edit-form]'); if (editForm) { editForm.hidden = true; } } setDrawerState(true); contentRoot.querySelectorAll('[data-lager-drawer-open]').forEach((button) => button.setAttribute('aria-expanded', String(button === trigger))); return; }";
|
||||
echo " if (event.target.closest('[data-lager-drawer-close]') || event.target.closest('[data-lager-drawer-backdrop]')) { closeDrawer(); }";
|
||||
echo " });";
|
||||
echo " document.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (!trigger || trigger.dataset.lagerDrawerType !== 'Produkt' || window.matchMedia('(max-width: 767px)').matches) { return; } const header = contentRoot.querySelector('[data-lager-drawer-warning-header]'); const title = contentRoot.querySelector('[data-lager-drawer-warning-title]'); if (!header || !title) { return; } const forecastWarning = trigger.dataset.lagerDrawerForecastWarning === 'true'; const mhdWarning = trigger.dataset.lagerDrawerForecastMhdWarning === 'true'; title.textContent = getLagerWarningTitle(trigger); header.classList.toggle('sg-card-segment--darkgreen', !forecastWarning && !mhdWarning); header.classList.toggle('sg-card-segment--signal-red', forecastWarning); header.classList.toggle('sg-card-segment--signal-yellow', mhdWarning); });";
|
||||
@@ -900,6 +1177,207 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const lagerTotalObserver = new MutationObserver(syncLagerDisplayedTotal);";
|
||||
echo " lagerTotalObserver.observe(contentRoot, { childList: true, subtree: true, characterData: true });";
|
||||
echo "}";
|
||||
echo "let marketsBindingsInstalled = false;";
|
||||
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;";
|
||||
echo " const getSearchValue = () => (getSearchInput()?.value || '').trim();";
|
||||
echo " const getSortColumn = () => getTable()?.dataset.marketsSortColumn || 'earliest_date';";
|
||||
echo " const getSortDirection = () => (getTable()?.dataset.marketsSortDirection || 'ASC').toUpperCase();";
|
||||
echo " const getCurrentLimit = () => parseInt(getTable()?.dataset.marketsLimit || '20', 10);";
|
||||
echo " const getPageSize = () => parseInt(getTable()?.dataset.marketsPageSize || '20', 10);";
|
||||
echo " const marketEscape = (value) => String(value ?? '').replace(/[&<>\"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[character] || character));";
|
||||
echo " const marketDisplay = (value) => { if (value === null || value === undefined || value === '') { return 'Nicht hinterlegt'; } const escaped = marketEscape(value); const safeMarkup = escaped.replace(/<br>/g, '<br>').replace(/<strong>/g, '<strong>').replace(/<\\/strong>/g, '</strong>').replace(/<a class="sg-hyperlink" href="(https?:\\/\\/.*?)" target="_blank" rel="noopener noreferrer">/g, '<a class=\"sg-hyperlink\" href=\"$1\" target=\"_blank\" rel=\"noopener noreferrer\">').replace(/<\\/a>/g, '</a>'); return safeMarkup.replace(/\\r?\\n/g, '<br>'); };";
|
||||
echo " const marketBoolean = (value) => value === true || value === 'true' || value === 1 || value === '1' ? 'Ja' : value === false || value === 'false' || value === 0 || value === '0' ? 'Nein' : 'Nicht hinterlegt';";
|
||||
echo " const marketMoney = (value) => value === null || value === undefined || value === '' ? 'Nicht hinterlegt' : 'CHF ' + Number(value).toFixed(2);";
|
||||
echo " const marketContactName = (contact) => { if (!contact) { return 'Nicht hinterlegt'; } return marketDisplay([contact.first_name, contact.last_name].filter(Boolean).join(' ')); };";
|
||||
echo " const marketDate = (value) => { if (!value) { return 'Nicht hinterlegt'; } const parts = String(value).split('-'); return parts.length === 3 ? parts[2] + '.' + parts[1] + '.' + parts[0] : marketEscape(value); };";
|
||||
echo " const marketStatusLabel = (value) => ({ open: 'Offen', waitlist: 'Warteliste', registered: 'Angemeldet', confirmation_pending: 'Bestätigung ausstehend', confirmed: 'Bestätigt', cancelled: 'Abgesagt', completed: 'Abgeschlossen' }[value] || value || 'Nicht hinterlegt');";
|
||||
echo " const normalizeMarketBooleanFields = (fields) => { ['table_available', 'table_rentable', 'own_table_required', 'shelter_available', 'power_available', 'water_available', 'lighting_available', 'direct_unload_possible'].forEach((field) => { if (fields[field] === '') { fields[field] = null; } }); return fields; };";
|
||||
echo " const marketStatusSegment = (value) => ({ waitlist: 'sg-card-segment--signal-red', registered: 'sg-card-segment--signal-yellow', confirmation_pending: 'sg-card-segment--signal-yellow', confirmed: 'sg-card-segment--signal-green' }[value] || 'sg-card-segment--gray');";
|
||||
echo " const marketChecklistSignalSegment = (items) => { const list = Array.isArray(items) ? items : []; const completed = list.filter((item) => item && item.is_completed).length; return completed === 0 ? 'sg-card-segment--signal-red' : completed === list.length ? 'sg-card-segment--signal-green' : 'sg-card-segment--signal-yellow'; };";
|
||||
echo " const marketRow = (label, value) => '<div class=\"market-detail-row\"><dt>' + marketEscape(label) + '</dt><dd>' + marketDisplay(value) + '</dd></div>';";
|
||||
echo " const initializeMunicipalityFields = (scope) => { scope.querySelectorAll('select[name=\"municipality_reference_id\"]').forEach((select) => { if (!select.querySelector('option[value=\"__new__\"]')) { const option = document.createElement('option'); option.value = '__new__'; option.textContent = 'Neue Gemeinde'; select.append(option); } const form = select.closest('form'); if (!form) { return; } let fields = form.querySelector('[data-market-new-municipality]'); if (!fields) { fields = document.createElement('div'); fields.className = 'sg-form-sections-card__field-group'; fields.dataset.marketNewMunicipality = ''; fields.hidden = true; fields.innerHTML = '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Gemeinde</span><input class=\"sg-interaction-element sg-input-single-line\" type=\"text\" name=\"municipality_name\" data-market-new-municipality-name></label>'; select.closest('.sg-form-sections-card__field-group')?.after(fields); } const sync = () => { const visible = select.value === '__new__'; fields.hidden = !visible; const input = fields.querySelector('[data-market-new-municipality-name]'); if (input) { input.disabled = !visible; input.setAttribute('aria-disabled', String(!visible)); } }; if (select.dataset.municipalityBound !== 'true') { select.addEventListener('change', sync); select.dataset.municipalityBound = 'true'; } sync(); }); };";
|
||||
echo " const syncMunicipalityReadValue = (scope) => { const drawer = scope.querySelector('[data-market-drawer]'); if (!drawer?.dataset.marketPayload) { return; } let payload = {}; try { payload = JSON.parse(drawer.dataset.marketPayload); } catch (error) { return; } const value = payload.occurrence?.municipality_name || 'Nicht hinterlegt'; scope.querySelectorAll('.market-detail-row').forEach((row) => { if (row.querySelector('dt')?.textContent?.trim() !== 'Gemeinde') { return; } const output = row.querySelector('dd'); if (output && output.textContent !== value) { output.textContent = value; } }); };";
|
||||
echo " const marketCard = (title, body, headerClass = 'sg-card-segment--darkgreen', actions = '', scope = '', id = '', values = null) => { const header = '<header class=\"sg-card-segment sg-card-segment--header ' + headerClass + ' sg-object-card__header\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">' + title + '</div>' + (actions ? '<div class=\"sg-component-row\">' + actions + '</div>' : '') + '</header>'; return '<article class=\"sg-card sg-card--content-card market-detail-card\" data-component=\"content-card\"' + (scope ? ' data-shared-editable-action-card' : '') + '>' + header + '<div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\"' + (scope ? ' data-shared-editable-card-read' : '') + '>' + body + '</div>' + (scope ? '<div class=\"sg-card-segment sg-card-segment--body sg-card-segment--gray\" data-shared-editable-card-form hidden>' + marketEditForm(scope, id, values) + '</div>' : '') + '</article>'; };";
|
||||
echo " const marketChecklistChapters = (checklist) => Array.isArray(checklist.chapters) ? checklist.chapters : [];";
|
||||
echo " const marketChecklistReadBody = (checklist) => { const items = Array.isArray(checklist.items) ? checklist.items : []; const chapterSections = marketChecklistChapters(checklist).map((chapter) => { const chapterItems = items.filter((item) => item.chapter_key === chapter.key); const rows = chapterItems.map((item) => { const index = items.indexOf(item); return '<div class=\"market-checklist-item\"><button class=\"sg-checkbox-field sg-checkbox-field--on-grey' + (item.is_completed ? ' sg-form-active' : '') + '\" type=\"button\" role=\"checkbox\" aria-checked=\"' + (item.is_completed ? 'true' : 'false') + '\" aria-label=\"Checklistenpunkt ' + (index + 1) + '\" data-checklist-read-checkbox=\"' + index + '\"><span class=\"sg-checkbox-field__mark\" aria-hidden=\"true\">✓</span></button><span class=\"sg-body\">' + marketEscape(item.label || '') + '</span></div>'; }).join(''); return '<section class=\"sg-form-sections-card__chapter market-checklist-chapter\" data-checklist-chapter=\"' + marketEscape(chapter.key) + '\"><h3 class=\"sg-form-sections-card__chapter-title\">' + marketEscape(chapter.label) + '</h3><div class=\"market-checklist-list\">' + rows + '</div></section>'; }).join(''); return '<div class=\"market-checklist-chapters\">' + chapterSections + '</div><div class=\"market-checklist-comment\"><div class=\"sg-strong\">Kommentar</div><p class=\"sg-body\">' + marketDisplay(checklist.comment) + '</p></div><p class=\"sg-body\" data-checklist-read-status aria-live=\"polite\"></p>'; };";
|
||||
echo " const marketChecklistEditBody = (checklist) => { const items = Array.isArray(checklist.items) ? checklist.items : []; const sections = marketChecklistChapters(checklist).map((chapter) => { const chapterItems = items.filter((item) => item.chapter_key === chapter.key); const rows = chapterItems.map((item) => '<label class=\"sg-labeled-input-row market-checklist-edit-row\" data-checklist-edit-item data-checklist-chapter-key=\"' + marketEscape(chapter.key) + '\"><span class=\"sg-label\"><button class=\"sg-checkbox-field sg-checkbox-field--on-grey' + (item.is_completed ? ' sg-form-active' : '') + '\" type=\"button\" role=\"checkbox\" aria-checked=\"' + (item.is_completed ? 'true' : 'false') + '\" aria-label=\"Checklistenpunkt abhaken\" data-checklist-edit-checkbox><span class=\"sg-checkbox-field__mark\" aria-hidden=\"true\">✓</span></button></span><span class=\"market-checklist-edit-control\"><input class=\"sg-interaction-element sg-input-single-line\" type=\"text\" value=\"' + marketEscape(item.label || '') + '\" data-checklist-label><button class=\"sg-interaction-element sg-button sg-button--warning market-checklist-delete\" type=\"button\" aria-label=\"Checklistenpunkt löschen\" data-checklist-delete-item>−</button></span></label>').join(''); const trailing = '<label class=\"sg-labeled-input-row market-checklist-edit-row market-checklist-edit-row--trailing\" data-checklist-edit-item data-checklist-trailing-item data-checklist-chapter-key=\"' + marketEscape(chapter.key) + '\"><span class=\"sg-label\"><button class=\"sg-checkbox-field sg-checkbox-field--on-grey\" type=\"button\" role=\"checkbox\" aria-checked=\"false\" aria-label=\"Neuen Checklistenpunkt abhaken\" data-checklist-edit-checkbox><span class=\"sg-checkbox-field__mark\" aria-hidden=\"true\">✓</span></button></span><span class=\"market-checklist-edit-control\"><input class=\"sg-interaction-element sg-input-single-line\" type=\"text\" value=\"\" aria-label=\"Neuer Checklistenpunkt\" data-checklist-label></span></label>'; return '<section class=\"sg-form-sections-card__chapter market-checklist-chapter\" data-checklist-chapter=\"' + marketEscape(chapter.key) + '\"><h3 class=\"sg-form-sections-card__chapter-title\">' + marketEscape(chapter.label) + '</h3><div class=\"market-checklist-edit-list\" data-checklist-edit-list data-checklist-chapter-key=\"' + marketEscape(chapter.key) + '\">' + rows + trailing + '</div></section>'; }).join(''); return sections + '<label class=\"sg-labeled-input-row market-checklist-comment-field\"><span class=\"sg-label\">Kommentar</span><textarea class=\"sg-interaction-element sg-input-multi-line\" name=\"comment\" rows=\"4\" data-checklist-comment>' + marketEscape(checklist.comment || '') + '</textarea></label>'; };";
|
||||
echo " const marketChecklistCard = (id, checklist) => { const headerClass = marketChecklistSignalSegment(checklist.items); const actions = '<button class=\"sg-interaction-element sg-button sg-button--active\" type=\"button\" data-shared-editable-card-toggle aria-expanded=\"false\">Anzeigen</button>' + marketEditButton('checklist', id); return '<article class=\"sg-card sg-card--content-card market-detail-card\" data-component=\"content-card\" data-shared-editable-action-card data-shared-editable-card-collapsible data-shared-editable-card-collapsed data-checklist-card><header class=\"sg-card-segment sg-card-segment--header ' + headerClass + ' sg-object-card__header\"><div class=\"sg-strong\">Checkliste</div><div class=\"sg-component-row\">' + actions + '</div></header><div class=\"sg-card-segment sg-card-segment--body sg-card-segment--gray\" data-shared-editable-card-read hidden>' + marketChecklistReadBody(checklist) + '</div><div class=\"sg-card-segment sg-card-segment--body sg-card-segment--gray\" data-shared-editable-card-form hidden><form class=\"sg-form-sections-card market-checklist-form\" data-market-checklist-form data-market-checklist-id=\"' + marketEscape(id) + '\"><div class=\"sg-form-sections-card__body\" data-pattern-part=\"form-body\">' + marketChecklistEditBody(checklist) + '</div><footer class=\"sg-form-sections-card__actions-segment\" data-pattern-part=\"form-actions-segment\"><div class=\"sg-form-sections-card__actions\" data-pattern-part=\"form-actions\"><button class=\"sg-interaction-element sg-button sg-button--active sg-form-sections-card__action\" type=\"button\" data-shared-editable-card-cancel>Abbrechen</button><button class=\"sg-interaction-element sg-button sg-button--process sg-form-sections-card__action\" type=\"submit\">Speichern</button></div></footer><p class=\"sg-body\" data-market-checklist-status aria-live=\"polite\"></p></form></div></article>'; };";
|
||||
echo " const setChecklistVisibility = (card, visible, showRead = visible) => { if (!window.SharedEditableActionCard) { return; } window.SharedEditableActionCard.setVisibility(card, visible); if (!showRead) { const readView = card?.querySelector('[data-shared-editable-card-read]'); if (readView) { readView.hidden = true; } } };";
|
||||
echo " const setChecklistSignal = (card, items) => { const header = card?.querySelector('header'); if (!header) { return; } header.classList.remove('sg-card-segment--signal-red', 'sg-card-segment--signal-yellow', 'sg-card-segment--signal-green'); header.classList.add(marketChecklistSignalSegment(items)); };";
|
||||
echo " const marketStatusCard = (id, values) => { const headerClass = marketStatusSegment(values.status); const textClass = headerClass === 'sg-card-segment--gray' ? '' : 'sg-text-on-dark'; const actions = marketEditButton('status', id); const header = '<header class=\"sg-card-segment sg-card-segment--header ' + headerClass + ' sg-object-card__header\" data-component-part=\"card-header\"><div class=\"sg-strong ' + textClass + '\">Status: ' + marketEscape(marketStatusLabel(values.status)) + '</div><div class=\"sg-component-row\">' + actions + '</div></header>'; return '<article class=\"sg-card sg-card--content-card market-detail-card\" data-component=\"content-card\" data-shared-editable-action-card>' + header + '<div class=\"sg-card-segment sg-card-segment--body sg-card-segment--gray\" data-shared-editable-card-form hidden>' + marketEditForm('status', id, values) + '</div></article>'; };";
|
||||
echo " const marketEditButton = () => '<button class=\"sg-interaction-element sg-button sg-button--active\" type=\"button\" data-shared-editable-card-edit>Bearbeiten</button>';";
|
||||
echo " const marketHumanizeError = (error) => { const message = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'; const labels = { table_available: 'Tisch vorhanden', table_rentable: 'Tisch mietbar', own_table_required: 'Eigener Tisch erforderlich', shelter_available: 'Überdachung', power_available: 'Strom', water_available: 'Wasser', lighting_available: 'Beleuchtung', direct_unload_possible: 'Direktes Entladen' }; const match = message.match(/^([a-z_]+) muss boolean sein\\.$/); return match && labels[match[1]] ? labels[match[1]] + ' muss Ja oder Nein sein.' : message; };";
|
||||
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_note', 'Hinweis Anmeldung', 'textarea'], ['registration_deadline', 'Anmeldefrist', 'date'], ['confirmation_date', 'Bestätigung', 'date'], ['payment_deadline', 'Zahlungsfrist', 'date'], ['payment_status', 'Zahlungsstatus', 'select', [['', 'Nicht hinterlegt'], ['paid', 'Bezahlt'], ['unpaid', 'Unbezahlt']]], ['payment_date', 'Zahlungsdatum', 'date'], ['paid_amount', 'Bezahlter Betrag', 'number'], ['payment_method_code', 'Zahlungsart', 'text'], ['gross_cost', 'Gesamtkosten brutto', 'readonly'], ['net_cost', 'Gesamtkosten netto', 'readonly'], ['tax_cost', 'Steuern', 'readonly'], ['open_cost', 'Offener Betrag', 'readonly'], ['cost_items_text', 'Kostenpositionen', 'readonly-textarea']], infrastructure: [['stand_size', 'Standgrösse', 'text'], ['table_available', 'Tisch vorhanden', 'select', boolOptions], ['table_rentable', 'Tisch mietbar', 'select', boolOptions], ['own_table_required', 'Eigener Tisch erforderlich', 'select', boolOptions], ['shelter_available', 'Überdachung', 'select', boolOptions], ['power_available', 'Strom', 'select', boolOptions], ['water_available', 'Wasser', 'select', boolOptions], ['lighting_available', 'Beleuchtung', 'select', boolOptions], ['other_infrastructure', 'Weitere Infrastruktur', 'textarea'], ['own_stand_requirements', 'Anforderungen an den eigenen Stand', 'textarea']], travel: [['travel_address', 'Reiseadresse', 'textarea'], ['travel_distance_km', 'Distanz (km)', 'number'], ['parking_options', 'Parkmöglichkeiten', 'textarea'], ['delivery_access', 'Anlieferung', 'textarea'], ['direct_unload_possible', 'Direktes Entladen', 'select', boolOptions], ['storage_options', 'Lagerung', 'textarea'], ['travel_notes', 'Besondere Anreisehinweise', 'textarea']], notes: [['cancellation_terms', 'Rücktrittsbedingungen', 'textarea'], ['revenue_share_terms', 'Umsatzbeteiligung', 'textarea'], ['other_participation_conditions', 'Teilnahmebedingungen', 'textarea'], ['notes', 'Notizen', 'textarea'], ['contract_folder_url', 'Vertragsordner', 'url']], day: [['date', 'Datum', 'date'], ['observations', 'Beobachtungen', 'textarea'], ['stand_start_time', 'Standzeit von', 'time'], ['stand_end_time', 'Standzeit bis', 'time'], ['setup_start_time', 'Aufbau von', 'time'], ['setup_end_time', 'Aufbau bis', 'time'], ['delivery_start_time', 'Anlieferung von', 'time'], ['delivery_end_time', 'Anlieferung bis', 'time'], ['gross_revenue_display', 'Umsatz', 'readonly'], ['sold_item_count_display', 'Verkaufte Artikel', 'readonly']] }; const requiredFields = { status: new Set(['status']), overview: new Set(), day: new Set(['d
Line truncated
|
||||
echo " const marketDeletePatternWithTitleAndDays = (id, title, body) => { const html = marketDeletePatternWithTitle(id, title, body); const template = document.createElement('template'); template.innerHTML = html; const target = template.content.querySelector('[data-market-delete-occurrence-id]'); const dayGroup = target?.querySelector('.market-days-group'); if (dayGroup) { const heading = document.createElement('h2'); heading.className = 'sg-heading-h2 market-days-group__title'; heading.textContent = 'Markttage'; dayGroup.before(heading); } return template.innerHTML; };";
|
||||
echo " const marketCreateForm = (marketDefinitions) => { const options = [['', 'Bitte auswählen'], ...(Array.isArray(marketDefinitions) ? marketDefinitions.map((market) => [market.id, market.name + (market.category_name ? ' – ' + market.category_name : '')]) : [])]; const fields = '<div class=\"sg-form-sections-card__field-group\">' + marketEditField('market_definition_id', 'Markt', 'select', '', options, true) + '</div><div class=\"sg-form-sections-card__field-group\">' + marketEditField('date', 'Erster Markttag', 'date', '', [], true) + '</div>'; return '<article class=\"sg-card sg-card--content-card market-detail-card\" data-component=\"content-card\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen sg-object-card__header\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Pflichtangaben für neue Marktdurchführung</div></header><div class=\"sg-card-segment sg-card-segment--body sg-card-segment--gray\"><div class=\"sg-form-sections-card-wrapper\" data-pattern=\"form-sections\"><form class=\"sg-form-sections-card market-create-form\" data-market-create-form=\"true\"><div class=\"sg-form-sections-card__body\" data-pattern-part=\"form-body\"><div class=\"sg-form-sections-card__section\">' + fields + '</div></div><footer class=\"sg-form-sections-card__actions-segment\" data-pattern-part=\"form-actions-segment\"><div class=\"sg-form-sections-card__actions\" data-pattern-part=\"form-actions\"><button class=\"sg-interaction-element sg-button sg-button--active sg-form-sections-card__action\" type=\"button\" data-market-create-cancel>Abbrechen</button><button class=\"sg-interaction-element sg-button sg-button--process sg-form-sections-card__action\" type=\"submit\">Speichern</button></div></footer><p class=\"sg-body\" data-market-create-status aria-live=\"polite\"></p></form></div></div></article>'; };";
|
||||
echo " const buildUrl = (params) => { const url = new URL(window.location.href); Object.entries(params).forEach(([key, value]) => { if (value === null || value === undefined || value === '') { url.searchParams.delete(key); } else { url.searchParams.set(key, String(value)); } }); return url; };";
|
||||
echo " const replaceFragment = (html) => { const doc = new DOMParser().parseFromString(html, 'text/html'); const fragment = doc.querySelector('[data-markets-content]'); if (!fragment) { return false; } contentRoot.innerHTML = fragment.outerHTML; return Boolean(getTable()); };";
|
||||
echo " const loadFragment = async (params) => { if (searchTimer) { window.clearTimeout(searchTimer); searchTimer = null; } const input = getSearchInput(); const wasFocused = input ? document.activeElement === input : false; const response = await fetch(buildUrl({ ...params, market_fragment: '1' }).toString(), { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { return; } const replaced = replaceFragment(await response.text()); if (!replaced) { return; } window.history.replaceState({}, '', buildUrl(params)); bindTable(); if (wasFocused) { const nextInput = getSearchInput(); nextInput?.focus({ preventScroll: true }); if (nextInput) { nextInput.setSelectionRange(nextInput.value.length, nextInput.value.length); } } };";
|
||||
echo " const syncSearchState = () => { const input = getSearchInput(); const wrap = input?.closest('[data-component=\'single-line-input\']'); if (!input || !wrap) { return; } const hasValue = input.value.trim() !== ''; wrap.setAttribute('data-has-value', String(hasValue)); wrap.dataset.componentState = hasValue ? 'active' : 'inactive-selectable'; };";
|
||||
echo " const bindTable = () => { const table = getTable(); if (!table) { return; } if (window.sgInitLargeTableColumnResize) { window.sgInitLargeTableColumnResize({ root: contentRoot }); } const input = getSearchInput(); const clearButton = table.querySelector('[data-markets-search-clear]'); if (input && input.dataset.marketsBound !== 'true') { input.dataset.marketsBound = 'true'; input.addEventListener('input', () => { syncSearchState(); if (searchTimer) { window.clearTimeout(searchTimer); } searchTimer = window.setTimeout(() => { void loadFragment({ market_search: getSearchValue(), market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getPageSize() }); }, 500); }); } if (clearButton && clearButton.dataset.marketsBound !== 'true') { clearButton.dataset.marketsBound = 'true'; clearButton.addEventListener('click', () => { const currentInput = getSearchInput(); if (currentInput) { currentInput.value = ''; } syncSearchState(); void loadFragment({ market_search: '', market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getPageSize() }); }); } syncSearchState(); };";
|
||||
echo " let marketMobileDetailHost = null; let marketOpeningMobileDetail = false; let marketDetailOpen = false;";
|
||||
echo " const marketMobileDetail = window.SharedMobileDetail?.create({ contentRoot, bodyClass: 'sg-market-mobile-detail-open', hostClass: 'sg-market-mobile-detail-host', hostDataAttribute: 'data-market-mobile-detail-host', drawerDataAttribute: 'data-market-mobile-drawer', backMarkup: '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\"><div class=\"sg-card-segment sg-card-segment--body\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-market-detail-back>zurück</button></div></div></article></div></div>', historyKey: 'marketView' });";
|
||||
echo " const getMarketDrawer = () => document.querySelector('[data-market-mobile-drawer]') || contentRoot.querySelector('[data-market-drawer]');";
|
||||
echo " const getMarketBackdrop = () => contentRoot.querySelector('[data-market-drawer-backdrop]');";
|
||||
echo " const setMarketDrawerState = (open) => { const drawer = getMarketDrawer(); const backdrop = getMarketBackdrop(); if (!drawer || !backdrop) { return; } const drawerOpen = Boolean(open); const backdropOpen = drawerOpen && !marketDetailOpen; drawer.dataset.open = String(drawerOpen); drawer.setAttribute('aria-hidden', String(!drawerOpen)); backdrop.dataset.open = String(backdropOpen); backdrop.setAttribute('aria-hidden', String(!backdropOpen)); document.body.classList.toggle('sg-card-list-page-drawer-open', backdropOpen); };";
|
||||
echo " const applyMarketTabs = (root) => { const group = root.querySelector('[data-pattern=\"card-gruppe-mit-tastennavigation\"] [role=\"tablist\"]'); if (group) { group.dataset.component = 'tab-navigation'; group.dataset.componentSize = 'large'; group.querySelectorAll('[data-market-day-tab]').forEach((tab) => { tab.dataset.componentPart = 'tab-button'; tab.classList.remove('sg-button--active'); }); } root.querySelectorAll('[data-market-day-tab]').forEach((tab) => { if (tab.dataset.marketTabBound === 'true') { return; } tab.dataset.marketTabBound = 'true'; tab.addEventListener('click', () => { const tabGroup = tab.closest('[data-pattern=\"card-gruppe-mit-tastennavigation\"]'); tabGroup?.querySelectorAll('[data-market-day-tab]').forEach((candidate) => candidate.setAttribute('aria-selected', String(candidate === tab))); tabGroup?.querySelectorAll('[role=\"tabpanel\"]').forEach((panel) => { panel.hidden = panel.id !== tab.getAttribute('aria-controls'); }); tab.focus(); }); }); };";
|
||||
echo " const syncMarketDayAddButton = (root = contentRoot) => { if (!root.querySelector('[data-market-inherit-row]')) { enhanceMarketDayInheritance(root); } const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; const days = Array.isArray(payload?.occurrence?.days) ? payload.occurrence.days.filter((day) => !day.__draft).slice().sort((left, right) => String(left.date).localeCompare(String(right.date))) : []; const nextDate = (date) => { const value = new Date(String(date) + 'T00:00:00Z'); value.setUTCDate(value.getUTCDate() + 1); return value.toISOString().slice(0, 10); }; root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=\"day\"]').forEach((form) => { const input = form.querySelector('[name=\"date\"]'); if (!input) { return; } const index = days.findIndex((day) => String(day.id) === String(form.dataset.marketEditId)); const minimum = form.dataset.marketEditDraft === 'true' ? days.at(-1)?.date : index > 0 ? days[index - 1].date : ''; input.min = minimum ? nextDate(minimum) : ''; }); const addButton = root.querySelector('[data-market-day-add]'); if (!addButton) { return; } const editing = Array.from(root.querySelectorAll('[data-market-edit-form]')).some((form) => form.closest('[data-shared-editable-card-form]')?.hidden === false); addButton.disabled = editing; addButton.setAttribute('aria-disabled', String(editing)); addButton.classList.toggle('sg-button--active', !editing); addButton.classList.toggle('sg-button--inactive', editing); };";
|
||||
echo " const enhanceMarketDayInheritance = (root) => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; const days = Array.isArray(payload?.occurrence?.days) ? payload.occurrence.days.filter((day) => !day.__draft).slice().sort((left, right) => String(left.date).localeCompare(String(right.date))) : []; const firstDayId = days[0]?.id; root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=\"day\"]').forEach((form) => { const dayId = form.dataset.marketEditId; if (!dayId || String(dayId) === String(firstDayId)) { return; } const dateGroup = form.querySelector('[name=\"date\"]')?.closest('.sg-form-sections-card__field-group'); const observationsGroup = form.querySelector('[name=\"observations\"]')?.closest('.sg-form-sections-card__field-group'); if (!dateGroup || !observationsGroup) { return; } let row = form.querySelector('[data-market-inherit-row]'); if (!row) { row = document.createElement('div'); row.className = 'sg-form-sections-card__field-group'; row.dataset.marketInheritRow = 'true'; row.innerHTML = '<label class=\"sg-checkbox-field-option sg-body\" data-component=\"checkbox-field\" data-component-state=\"default\"><button class=\"sg-checkbox-field\" type=\"button\" role=\"checkbox\" aria-checked=\"false\" aria-label=\"Ab hier alle Werte aus Tag 1 übernehmen\" data-market-inherit-toggle><span class=\"sg-checkbox-field__mark\" aria-hidden=\"true\">✓</span></button><span>Ab hier alle Werte aus Tag 1 übernehmen</span><input type=\"hidden\" name=\"inherit_from_first_day\" value=\"0\"></label>'; observationsGroup.after(row); row.querySelector('[data-market-inherit-toggle]').addEventListener('click', (event) => { event.stopImmediatePropagation(); const toggle = row.querySelector('[data-market-inherit-toggle]'); const checked = toggle.getAttribute('aria-checked') !== 'true'; toggle.setAttribute('aria-checked', String(checked)); toggle.classList.toggle('sg-form-active', checked); row.querySelector('[name=\"inherit_from_first_day\"]').value = checked ? '1' : '0'; form.querySelectorAll('input, select, textarea').forEach((control) => { if (control.name !== 'date' && control.name !== 'inherit_from_first_day' && control.name !== 'observations') { control.disabled = checked; control.setAttribute('aria-disabled', String(checked)); control.classList.toggle('sg-form-disabled', checked); control.classList.toggle('sg-input-single-line--disabled', checked && control.tagName === 'INPUT'); } }); syncMarketDayAddButton(root); }); } const day = days.find((candidate) => String(candidate.id) === String(dayId)); const inherited = Boolean(day?.inherit_from_first_day); const toggle = row.querySelector('[data-market-inherit-toggle]'); toggle.setAttribute('aria-checked', String(inherited)); toggle.classList.toggle('sg-form-active', inherited); row.querySelector('[name=\"inherit_from_first_day\"]').value = inherited ? '1' : '0'; form.querySelectorAll('input, select, textarea').forEach((control) => { if (control.name !== 'date' && control.name !== 'inherit_from_first_day' && control.name !== 'observations') { control.disabled = inherited; control.setAttribute('aria-disabled', String(inherited)); control.classList.toggle('sg-form-disabled', inherited); control.classList.toggle('sg-input-single-line--disabled', inherited && control.tagName === 'INPUT'); } }); }); };";
|
||||
echo " const marketDayDeleteConfirmation = (id, label) => '<article class=\"sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card\" aria-label=\"Löschbestätigung für Markttag\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"market-day-delete-title-' + marketEscape(id) + '\" data-overlay-dialog=\"delete\" data-market-day-delete-confirmation data-shared-editable-card-delete-confirmation hidden><div class=\"sg-card-segment sg-card-segment--body sg-delete-confirmation-pattern__body\"><p class=\"sg-body sg-delete-confirmation-pattern__text\" id=\"market-day-delete-title-' + marketEscape(id) + '\"><strong>Möchtest du den Markttag ' + marketEscape(label) + ' wirklich löschen?</strong></p><p class=\"sg-body sg-delete-confirmation-pattern__text\">Du kannst das nicht rückgängig machen. Bestätige durch Eingabe von <span class=\"sg-delete-confirmation-pattern__code\">DELETE</span>.</p><label class=\"sg-labeled-input-row sg-delete-confirmation-pattern__input-row\"><span class=\"sg-label\">Bestätigung</span><input class=\"sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable\" type=\"text\" placeholder=\"DELETE\" aria-label=\"Löschbestätigung durch DELETE\" data-shared-editable-card-delete-input></label><p class=\"sg-body\" data-market-day-delete-status aria-live=\"polite\"></p><div class=\"sg-delete-confirmation-pattern__actions\"><button class=\"sg-interaction-element sg-button sg-button--active\" type=\"button\" data-shared-editable-card-delete-cancel>Abbrechen</button><button class=\"sg-interaction-element sg-button sg-button--process sg-button--process-inactive\" type=\"button\" disabled aria-disabled=\"true\" data-shared-editable-card-delete-submit data-market-day-delete-submit>Löschen</button></div></div></article>';";
|
||||
echo " const enhanceMarketDayDeleteControls = (root) => { const cards = Array.from(root.querySelectorAll('.market-days-group .sg-content-cards-group > [data-shared-editable-action-card]')); cards.forEach((card, index) => { if (index === 0 || card.querySelector('[data-market-edit-form][data-market-edit-draft=\"true\"]')) { return; } const form = card.querySelector('[data-market-edit-form][data-market-edit-scope=\"day\"]'); const actions = card.querySelector('.sg-object-card__header .sg-component-row'); if (!form || !actions || actions.querySelector('[data-market-day-delete-button]')) { return; } const dayId = form.dataset.marketEditId || ''; const label = card.querySelector('.sg-object-card__header .sg-strong')?.textContent?.trim() || 'diesen Markttag'; if (!dayId) { return; } const stage = document.createElement('div'); stage.className = 'sg-delete-confirmation-pattern__stage sg-delete-confirmation-pattern__host'; stage.dataset.pattern = 'overlay-card'; stage.dataset.dialogOpen = 'false'; card.classList.add('sg-delete-confirmation-pattern__target'); card.parentNode.replaceChild(stage, card); stage.appendChild(card); card.dataset.marketDayDeleteId = dayId; card.dataset.sharedEditableDeleteToken = 'DELETE'; actions.insertAdjacentHTML('afterbegin', '<button class=\"sg-interaction-element sg-button sg-button--warning\" type=\"button\" data-component=\"button\" data-component-variant=\"warning\" data-shared-editable-card-delete data-market-day-delete-button>Löschen</button>'); card.insertAdjacentHTML('beforeend', marketDayDeleteConfirmation(dayId, label)); }); };";
|
||||
echo " const rerenderMarketDetail = (payload) => { const drawer = getMarketDrawer(); const content = drawer?.querySelector('[data-market-drawer-content]'); if (!drawer || !content) { return; } drawer.dataset.marketPayload = JSON.stringify(payload); content.innerHTML = renderMarketDetailWithActions(payload); applyMarketTabs(content); enhanceMarketDayDeleteControls(content); syncMarketDayAddButton(content); };";
|
||||
echo " const addMarketDayDraft = () => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; if (!payload?.occurrence) { return; } const days = Array.isArray(payload.occurrence.days) ? payload.occurrence.days : []; if (days.some((day) => day.__draft)) { return; } days.push({ id: 'draft-' + Date.now(), date: '', __draft: true }); payload.occurrence.days = days; rerenderMarketDetail(payload); const draftEdit = contentRoot.querySelector('[data-market-edit-form][data-market-edit-draft=\"true\"]')?.closest('[data-shared-editable-action-card]')?.querySelector('[data-shared-editable-card-edit]'); draftEdit?.click(); window.setTimeout(() => syncMarketDayAddButton(contentRoot), 0); };";
|
||||
echo " const discardMarketDayDraft = () => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; if (!payload?.occurrence || !Array.isArray(payload.occurrence.days)) { return; } const days = payload.occurrence.days.filter((day) => !day.__draft); if (days.length === payload.occurrence.days.length) { return; } payload.occurrence.days = days; rerenderMarketDetail(payload); };";
|
||||
echo " const closeMarketDrawer = () => { setMarketDrawerState(false); contentRoot.querySelectorAll('[data-market-drawer-open]').forEach((button) => button.setAttribute('aria-expanded', 'false')); };";
|
||||
echo " const renderMarketDetail = (payload) => { const market = payload.market || {}; const occurrence = payload.occurrence || {}; const days = Array.isArray(occurrence.days) ? occurrence.days : []; const hasDraftDay = days.some((day) => day.__draft); const organizer = occurrence.organizer || {}; const organization = organizer.organization || {}; const contactPerson = organizer.contact_person || {}; const website = market.website_url ? '<a class=\"sg-hyperlink\" href=\"' + marketEscape(market.website_url) + '\" target=\"_blank\" rel=\"noopener\">' + marketEscape(market.website_url) + '</a>' : 'Nicht hinterlegt'; const contract = occurrence.contract_folder_url ? '<a class=\"sg-hyperlink\" href=\"' + marketEscape(occurrence.contract_folder_url) + '\" target=\"_blank\" rel=\"noopener\">Vertragsordner öffnen</a>' : 'Nicht hinterlegt'; const overview = marketRow('Markt', market.name) + marketRow('Kategorie', market.category_name || market.category_code) + '<div class=\"market-detail-row\"><dt>Website</dt><dd>' + website + '</dd></div>' + marketRow('Ort', occurrence.place) + marketRow('Adresse', occurrence.address) + marketRow('Gemeinde', occurrence.municipality_reference_id) + marketRow('Veranstalter', organization.name) + marketRow('Kontaktperson', marketContactName(contactPerson)) + marketRow('Kontakt E-Mail', contactPerson.email) + marketRow('Kontakt Telefon', contactPerson.phone); const dates = days.map((day) => marketDate(day.date)).filter((date) => date !== 'Nicht hinterlegt').join(' – '); const costSummary = occurrence.cost_summary || {}; const participation = marketRow('Anmeldung / Anfrage', occurrence.request_or_registration_date) + marketRow('Hinweis Anmeldung', occurrence.registration_note) + marketRow('Anmeldefrist', occurrence.registration_deadline) + marketRow('Bestätigung', occurrence.confirmation_date) + marketRow('Zahlungsfrist', occurrence.payment_deadline) + marketRow('Zahlungsstatus', occurrence.payment_status) + marketRow('Zahlungsdatum', occurrence.payment_date) + marketRow('Bezahlter Betrag', marketMoney(occurrence.paid_amount)) + marketRow('Zahlungsart', occurrence.payment_method_code) + marketRow('Gesamtkosten brutto', marketMoney(costSummary.gross_amount)) + marketRow('Gesamtkosten netto', marketMoney(costSummary.net_amount)) + marketRow('Steuern', marketMoney(costSummary.tax_amount)) + marketRow('Offener Betrag', marketMoney(costSummary.open_amount)) + '<div class=\"market-detail-row\"><dt>Kostenpositionen</dt><dd>' + marketDisplay(occurrence.cost_items_text) + '</dd></div>'; const infrastructure = marketRow('Standgrösse', occurrence.stand_size) + marketRow('Tisch vorhanden', marketBoolean(occurrence.table_available)) + marketRow('Tisch mietbar', marketBoolean(occurrence.table_rentable)) + marketRow('Eigener Tisch erforderlich', marketBoolean(occurrence.own_table_required)) + marketRow('Überdachung', marketBoolean(occurrence.shelter_available)) + marketRow('Strom', marketBoolean(occurrence.power_available)) + marketRow('Wasser', marketBoolean(occurrence.water_available)) + marketRow('Beleuchtung', marketBoolean(occurrence.lighting_available)) + marketRow('Weitere Infrastruktur', occurrence.other_infrastructure) + marketRow('Anforderungen an den eigenen Stand', occurrence.own_stand_requirements); const travel = marketRow('Reiseadresse', occurrence.travel_address) + marketRow('Distanz (km)', occurrence.travel_distance_km) + marketRow('Parkmöglichkeiten', occurrence.parking_options) + marketRow('Anlieferung', occurrence.delivery_access) + marketRow('Direktes Entladen', marketBoolean(occurrence.direct_unload_possible)) + marketRow('Lagerung', occurrence.storage_options) + marketRow('Besondere Anreisehinweise', occurrence.travel_notes); const notes = marketRow('Teilnahmebedingungen', occurrence.other_participation_conditions) + marketRow('Rücktrittsbedingungen', occurrence.cancellation_terms) + marketRow('Umsatzbeteiligung', occurrence.revenue_share_terms) + marketRow('Notizen', occurrence.notes) + '<div class=\"market-detail-row\"><dt>Vertragsordner</dt><dd>' + contract + '</dd></div>'; const salesSummary = occurrence.sales_summary || {}; const dayTabs = days.map((day, index) => { const label = day.__draft ? 'Neuer Tag' : marketDate(day.date); const selected = day.__draft || (!hasDraftDay && index === 0); return '<button class=\"sg-interaction-element sg-button sg-button--active sg-tab-button\" type=\"button\" role=\"tab\" aria-selected=\"' + (selected ? 'true' : 'false') + '\" aria-controls=\"market-day-panel-' + day.id + '\" id=\"market-day-tab-' + day.id + '\" data-market-day-tab=\"true\">' + label + '</button>'; }).join(''); const addDayTab = '<button class=\"sg-interaction-element sg-button sg-button--active sg-tab-button market-day-add-button\" type=\"button\" data-market-day-add=\"true\" aria-label=\"Markttag hinzufügen\" title=\"Markttag hinzufügen\">+</button>'; const dayPanels = days.map((day, index) => { const label = day.__draft ? 'Neuer Tag' : marketDate(day.da
Line truncated
|
||||
echo " const reorderMarketStatusCard = (html) => { const template = document.createElement('template'); template.innerHTML = html; const target = template.content.querySelector('[data-market-delete-occurrence-id]'); const cards = target ? Array.from(target.children).filter((child) => child.matches('.market-detail-card')) : []; if (cards.length > 1) { target.insertBefore(cards[0], cards[1].nextSibling); } return template.innerHTML; };";
|
||||
echo " const renderMarketDetailWithActions = (payload) => reorderMarketStatusCard(marketDeletePatternWithTitleAndDays(payload.occurrence?.id || '', marketDrawerTitle(payload), renderMarketDetail(payload)));";
|
||||
echo " const ensureChecklistTrailingItem = (form, list) => { const targetList = list || form?.querySelector('[data-checklist-edit-list]'); if (!targetList) { return; } const trailing = targetList.querySelector('[data-checklist-trailing-item]'); if (!trailing || (trailing.querySelector('[data-checklist-label]')?.value || '').trim() !== '') { const item = document.createElement('label'); item.className = 'sg-labeled-input-row market-checklist-edit-row market-checklist-edit-row--trailing'; item.dataset.checklistEditItem = ''; item.dataset.checklistTrailingItem = ''; item.dataset.checklistChapterKey = targetList.dataset.checklistChapterKey || ''; item.innerHTML = '<span class=\"sg-label\"><button class=\"sg-checkbox-field sg-checkbox-field--on-grey\" type=\"button\" role=\"checkbox\" aria-checked=\"false\" aria-label=\"Neuen Checklistenpunkt abhaken\" data-checklist-edit-checkbox><span class=\"sg-checkbox-field__mark\" aria-hidden=\"true\">✓</span></button></span><span class=\"market-checklist-edit-control\"><input class=\"sg-interaction-element sg-input-single-line\" type=\"text\" value=\"\" aria-label=\"Neuer Checklistenpunkt\" data-checklist-label></span>'; targetList.append(item); } };";
|
||||
echo " const setChecklistCheckbox = (button, checked) => { if (!button) { return; } button.setAttribute('aria-checked', String(checked)); button.classList.toggle('sg-form-active', checked); };";
|
||||
echo " const persistChecklistReadToggle = async (button) => { const drawer = getMarketDrawer(); const card = button.closest('[data-checklist-card]'); const status = card?.querySelector('[data-checklist-read-status]'); const index = Number(button.dataset.checklistReadCheckbox); let payload = {}; try { payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : {}; } catch (error) { return; } const sourceItems = Array.isArray(payload.occurrence?.checklist?.items) ? payload.occurrence.checklist.items : []; if (!sourceItems[index]) { return; } const previous = button.getAttribute('aria-checked') === 'true'; const next = !previous; setChecklistCheckbox(button, next); setChecklistCheckbox(card.querySelectorAll('[data-checklist-edit-checkbox]')[index], next); const items = Array.from(card.querySelectorAll('[data-checklist-read-checkbox]')).map((checkbox, itemIndex) => ({ chapter_key: sourceItems[itemIndex]?.chapter_key || '', label: sourceItems[itemIndex]?.label || '', is_completed: checkbox.getAttribute('aria-checked') === 'true' })); setChecklistSignal(card, items); button.disabled = true; if (status) { status.textContent = 'Speichern läuft …'; } try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'update_checklist', id: payload.occurrence.id, items: items.map((item) => ({ chapter_key: item.chapter_key, label: item.label || '', is_completed: Boolean(item.is_completed) })), comment: payload.occurrence.checklist.comment || '' }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } payload.occurrence.checklist = result.checklist; if (drawer) { drawer.dataset.marketPayload = JSON.stringify(payload); } if (status) { status.textContent = ''; } } catch (error) { setChecklistCheckbox(button, previous); setChecklistCheckbox(card.querySelectorAll('[data-checklist-edit-checkbox]')[index], previous); const rollbackItems = Array.from(card.querySelectorAll('[data-checklist-read-checkbox]')).map((checkbox, itemIndex) => ({ chapter_key: sourceItems[itemIndex]?.chapter_key || '', label: sourceItems[itemIndex]?.label || '', is_completed: checkbox.getAttribute('aria-checked') === 'true' })); setChecklistSignal(card, rollbackItems); if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'; } } finally { button.disabled = false; } };";
|
||||
echo " document.addEventListener('click', (event) => { const checkbox = event.target.closest('[data-checklist-edit-checkbox]'); if (checkbox) { event.preventDefault(); setChecklistCheckbox(checkbox, checkbox.getAttribute('aria-checked') !== 'true'); const editCard = checkbox.closest('[data-checklist-card]'); setChecklistSignal(editCard, Array.from(editCard?.querySelectorAll('[data-checklist-edit-item]') || []).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' }))); return; } const readCheckbox = event.target.closest('[data-checklist-read-checkbox]'); if (readCheckbox) { event.preventDefault(); void persistChecklistReadToggle(readCheckbox); return; } const deleteItem = event.target.closest('[data-checklist-delete-item]'); if (deleteItem) { const list = deleteItem.closest('[data-checklist-edit-list]'); deleteItem.closest('[data-checklist-edit-item]')?.remove(); ensureChecklistTrailingItem(deleteItem.closest('[data-market-checklist-form]'), list); const deleteCard = deleteItem.closest('[data-checklist-card]'); setChecklistSignal(deleteCard, Array.from(deleteCard?.querySelectorAll('[data-checklist-edit-item]') || []).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' }))); } });";
|
||||
echo " document.addEventListener('input', (event) => { const input = event.target.closest('[data-market-checklist-form] [data-checklist-label]'); if (!input) { return; } const form = input.closest('[data-market-checklist-form]'); const item = input.closest('[data-checklist-edit-item]'); const list = input.closest('[data-checklist-edit-list]'); if (item?.hasAttribute('data-checklist-trailing-item') && input.value.trim() !== '') { item.removeAttribute('data-checklist-trailing-item'); } ensureChecklistTrailingItem(form, list); const card = input.closest('[data-checklist-card]'); setChecklistSignal(card, Array.from(card?.querySelectorAll('[data-checklist-edit-item]') || []).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' }))); });";
|
||||
echo " document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-market-checklist-form]'); if (!form) { return; } event.preventDefault(); event.stopImmediatePropagation(); const status = form.querySelector('[data-market-checklist-status]'); const checklistWasVisible = window.SharedEditableActionCard?.isVisible(form.closest('[data-checklist-card]')) === true; const items = Array.from(form.querySelectorAll('[data-checklist-edit-item]')).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ chapter_key: item.dataset.checklistChapterKey || item.closest('[data-checklist-edit-list]')?.dataset.checklistChapterKey || '', label: item.querySelector('[data-checklist-label]')?.value || '', is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' })); if (status) { status.textContent = 'Speichern läuft …'; } try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'update_checklist', id: form.dataset.marketChecklistId, items, comment: form.querySelector('[data-checklist-comment]')?.value || '' }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } const trigger = contentRoot.querySelector('[data-market-drawer-open][aria-expanded=\"true\"]'); if (trigger) { await openMarketDrawer(trigger); if (checklistWasVisible) { setChecklistVisibility(getMarketDrawer()?.querySelector('[data-checklist-card]'), true); } } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'; } } });";
|
||||
echo " const marketDrawerTitle = (payload) => { const marketName = payload.market?.name || 'Marktdurchführung'; const dates = (Array.isArray(payload.occurrence?.days) ? payload.occurrence.days : []).map((day) => marketDate(day.date)).filter(Boolean); return dates.length ? marketName + ', Marktdurchführung ' + dates.join(', ') : marketName; };";
|
||||
echo " const openMarketDrawer = async (trigger) => { const drawer = getMarketDrawer(); if (!drawer) { return; } const title = drawer.querySelector('[data-market-drawer-title]'); const content = drawer.querySelector('[data-market-drawer-content]'); setMarketDrawerState(true); if (title) { title.textContent = 'Marktdurchführung'; } if (content) { content.innerHTML = '<p class=\"sg-body\">Marktdetails werden geladen …</p>'; } try { const response = await fetch('/api/market-occurrence.php?id=' + encodeURIComponent(trigger.dataset.marketOccurrenceId || ''), { credentials: 'same-origin' }); const payload = await response.json(); if (!response.ok || !payload.ok) { throw new Error(payload.error || 'Marktdetails konnten nicht geladen werden.'); } drawer.dataset.marketPayload = JSON.stringify(payload); if (title) { title.textContent = marketDrawerTitle(payload); } if (content) { content.innerHTML = renderMarketDetailWithActions(payload); applyMarketTabs(content); syncMarketDayAddButton(content); } } catch (error) { if (content) { content.innerHTML = '<p class=\"sg-body\">' + marketEscape(error.message) + '</p>'; } } contentRoot.querySelectorAll('[data-market-drawer-open]').forEach((button) => button.setAttribute('aria-expanded', String(button === trigger))); };";
|
||||
echo " const openNewMarketDrawer = async () => { const drawer = getMarketDrawer(); if (!drawer) { return; } const title = drawer.querySelector('[data-market-drawer-title]'); const content = drawer.querySelector('[data-market-drawer-content]'); setMarketDrawerState(true); if (title) { title.textContent = 'Neue Marktdurchführung'; } if (content) { content.innerHTML = '<p class=\"sg-body\">Pflichtangaben werden geladen …</p>'; } try { const response = await fetch('/api/market-occurrence.php?action=create_options', { credentials: 'same-origin' }); const payload = await response.json(); if (!response.ok || !payload.ok) { throw new Error(payload.error || 'Pflichtangaben konnten nicht geladen werden.'); } if (content) { content.innerHTML = marketCreateForm(payload.market_definitions || []); } } catch (error) { if (content) { content.innerHTML = '<p class=\"sg-body\">' + marketEscape(error.message) + '</p>'; } } };";
|
||||
echo " const restoreMarketList = () => { marketMobileDetail?.restore(); marketMobileDetailHost = null; marketDetailOpen = false; bindTable(); };";
|
||||
echo " const openMarketMobileDetail = (trigger) => { restoreMarketList(); const sourceDrawer = contentRoot.querySelector('[data-market-drawer]'); if (!sourceDrawer) { return; } const host = document.createElement('div'); host.className = 'sg-market-mobile-detail-host'; host.dataset.marketMobileDetailHost = 'true'; host.innerHTML = '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\"><div class=\"sg-card-segment sg-card-segment--body\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-market-detail-back>zurück</button></div></div></article></div></div>'; sourceDrawer.dataset.marketMobileDrawer = 'true'; sourceDrawer.dataset.open = 'true'; sourceDrawer.setAttribute('aria-hidden', 'false'); host.appendChild(sourceDrawer); marketMobileDetailHost = host; marketDetailOpen = true; marketMobileDetail?.mount(sourceDrawer, trigger, host); if (window.SharedEditableActionCard) { window.SharedEditableActionCard.init(host); } marketOpeningMobileDetail = true; void openMarketDrawer(trigger).finally(() => { marketOpeningMobileDetail = false; }); };";
|
||||
echo " const deleteMarketOccurrence = async (submit) => { if (submit.disabled) { return; } const card = submit.closest('[data-shared-editable-action-card]'); const confirmation = Array.from(card?.children || []).find((child) => child.matches('[data-shared-editable-card-delete-confirmation]')); const input = confirmation?.querySelector('[data-shared-editable-card-delete-input]'); const status = confirmation?.querySelector('[data-market-delete-status]'); const occurrenceId = card?.dataset.marketDeleteOccurrenceId || ''; if (!card || !input || input.value !== 'DELETE' || !occurrenceId) { return; } if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'delete', id: occurrenceId, confirmation: input.value }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Löschen fehlgeschlagen.'); } closeMarketDrawer(); if (marketDetailOpen) { restoreMarketList(); } await loadFragment({ market_search: getSearchValue(), market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getCurrentLimit() }); } catch (error) { if (status) { status.textContent = marketEscape(error.message); } submit.disabled = false; submit.setAttribute('aria-disabled', 'false'); } };";
|
||||
echo " const deleteMarketDay = async (submit) => { if (submit.disabled) { return; } const card = submit.closest('[data-shared-editable-action-card]'); const input = card?.querySelector('[data-shared-editable-card-delete-input]'); const status = card?.querySelector('[data-market-day-delete-status]'); const dayId = card?.dataset.marketDayDeleteId || ''; if (!card || !input || input.value !== 'DELETE' || !dayId) { return; } if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'delete_day', id: dayId, confirmation: input.value }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Löschen fehlgeschlagen.'); } const trigger = contentRoot.querySelector('[data-market-drawer-open][aria-expanded=\"true\"]'); if (trigger) { await openMarketDrawer(trigger); } } catch (error) { if (status) { status.textContent = marketEscape(error.message); } submit.disabled = false; submit.setAttribute('aria-disabled', 'false'); } };";
|
||||
echo " const marketDayDeleteObserver = new MutationObserver(() => enhanceMarketDayDeleteControls(contentRoot)); marketDayDeleteObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
echo " const municipalityFieldObserver = new MutationObserver(() => initializeMunicipalityFields(contentRoot)); municipalityFieldObserver.observe(contentRoot, { childList: true, subtree: true }); initializeMunicipalityFields(contentRoot);";
|
||||
echo " const municipalityReadObserver = new MutationObserver(() => syncMunicipalityReadValue(contentRoot)); municipalityReadObserver.observe(contentRoot, { childList: true, subtree: true }); syncMunicipalityReadValue(contentRoot);";
|
||||
echo " if (!marketsBindingsInstalled) {";
|
||||
echo " marketsBindingsInstalled = true;";
|
||||
echo " document.addEventListener('click', (event) => { const 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 = normalizeMarketBooleanFields(Object.fromEntries(new FormData(form).entries())); const occurrenceId = form.dataset.marketEditId || ''; if (status) { status.textContent = 'Speichern läuft …'; } try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ scope: form.dataset.marketEditScope, id: occurrenceId, fields }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } await loadFragment({ market_search: getSearchValue(), market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getCurrentLimit() }); const refreshedTrigger = contentRoot.querySelector('[data-market-drawer-open][data-market-occurrence-id=\"' + occurrenceId + '\"]'); if (refreshedTrigger) { await openMarketDrawer(refreshedTrigger); } } catch (error) { if (status) { status.textContent = marketEscape(error.message); } } });";
|
||||
echo " document.addEventListener('click', (event) => { if (event.target.closest('[data-market-drawer-backdrop]')) { closeMarketDrawer(); } });";
|
||||
echo " document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { closeMarketDrawer(); restoreMarketList(); } });";
|
||||
echo " window.addEventListener('popstate', () => { if (marketDetailOpen) { restoreMarketList(); } });";
|
||||
echo " }";
|
||||
echo " const normalizeOccurrenceOverviewForm = (root) => { root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=overview]').forEach((form) => { if (form.dataset.marketOccurrenceFieldsNormalized === 'true') { return; } const organizerSelect = form.querySelector('[name=organization_contact_id]'); const contactSelect = form.querySelector('[name=contact_person_id]'); const organizerField = organizerSelect?.closest('.sg-form-sections-card__field-group'); const contactField = contactSelect?.closest('.sg-form-sections-card__field-group'); if (organizerField && organizerSelect) { const value = organizerSelect.selectedOptions[0]?.textContent || 'Nicht hinterlegt'; organizerField.innerHTML = '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Veranstalter</span><input class=\"sg-interaction-element sg-input-single-line sg-input-single-line--disabled sg-form-disabled\" type=\"text\" name=\"organizer_name\" value=\"' + marketEscape(value) + '\" disabled aria-disabled=\"true\"></label>'; } if (contactField && contactSelect) { const value = contactSelect.selectedOptions[0]?.textContent || ''; contactField.innerHTML = '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Kontaktperson</span><input class=\"sg-interaction-element sg-input-single-line\" type=\"text\" name=\"contact_person_name\" value=\"' + marketEscape(value) + '\"></label>'; } form.dataset.marketOccurrenceFieldsNormalized = 'true'; }); };";
|
||||
echo " const marketOccurrenceFieldObserver = new MutationObserver(() => normalizeOccurrenceOverviewForm(contentRoot)); marketOccurrenceFieldObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
echo " const normalizeMarketSimpleCostFields = (root) => { root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation]').forEach((form) => { if (form.dataset.marketSimpleCostsNormalized === 'true') { return; } const drawer = getMarketDrawer(); let payload = {}; try { payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : {}; } catch (error) {} const item = Array.isArray(payload.occurrence?.cost_items) ? payload.occurrence.cost_items[0] : null; const total = form.querySelector('[name=gross_cost]'); const text = form.querySelector('[name=cost_items_text]'); const totalLabel = total?.closest('.sg-labeled-input-row')?.querySelector('.sg-label'); const textLabel = text?.closest('.sg-labeled-input-row')?.querySelector('.sg-label'); if (total) { total.disabled = false; total.removeAttribute('disabled'); total.type = 'number'; total.step = '0.01'; total.min = '0'; total.required = true; total.classList.remove('sg-input-single-line--disabled', 'sg-form-disabled'); total.value = payload.occurrence?.cost_summary?.gross_amount ?? item?.gross_amount ?? ''; } if (text) { text.disabled = false; text.removeAttribute('disabled'); text.classList.remove('sg-form-disabled'); text.value = payload.occurrence?.cost_items_text ?? item?.description ?? ''; } if (totalLabel) { totalLabel.textContent = 'Gesamtkosten'; } if (textLabel) { textLabel.textContent = 'Kostenpositionen'; } form.dataset.marketSimpleCostsNormalized = 'true'; }); };";
|
||||
echo " const marketSimpleCostFieldsObserver = new MutationObserver(() => normalizeMarketSimpleCostFields(contentRoot)); marketSimpleCostFieldsObserver.observe(contentRoot, { childList: true, subtree: true }); normalizeMarketSimpleCostFields(contentRoot);";
|
||||
echo " const normalizeMarketTaxFields = (root) => { root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation]').forEach((form) => { if (form.dataset.marketTaxFieldsNormalized === 'true') { return; } const total = form.querySelector('[name=gross_cost]'); const totalGroup = total?.closest('.sg-form-sections-card__field-group'); if (!totalGroup) { return; } const drawer = getMarketDrawer(); let payload = {}; try { payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : {}; } catch (error) {} const item = Array.isArray(payload.occurrence?.cost_items) ? payload.occurrence.cost_items[0] : null; const taxStatusGroup = document.createElement('div'); taxStatusGroup.className = 'sg-form-sections-card__field-group'; taxStatusGroup.innerHTML = marketEditField('tax_status', 'Steuerstatus', 'select', item?.tax_status || 'taxable', [['taxable', 'Steuerpflichtig'], ['tax_free', 'Steuerfrei']], true); const taxRateGroup = document.createElement('div'); taxRateGroup.className = 'sg-form-sections-card__field-group'; taxRateGroup.innerHTML = marketEditField('tax_rate', 'Steuersatz', 'number', item?.tax_rate ?? '8.10', [], true); totalGroup.after(taxStatusGroup, taxRateGroup); const taxStatus = taxStatusGroup.querySelector('[name=tax_status]'); const taxRate = taxRateGroup.querySelector('[name=tax_rate]'); const sync = () => { const taxFree = taxStatus?.value === 'tax_free'; if (taxFree && taxRate) { taxRate.value = '0.00'; } if (taxRate) { taxRate.disabled = taxFree; taxRate.setAttribute('aria-disabled', String(taxFree)); } }; taxStatus?.addEventListener('change', sync); sync(); form.dataset.marketTaxFieldsNormalized = 'true'; }); };";
|
||||
echo " const marketTaxFieldsObserver = new MutationObserver(() => normalizeMarketTaxFields(contentRoot)); marketTaxFieldsObserver.observe(contentRoot, { childList: true, subtree: true }); normalizeMarketTaxFields(contentRoot);";
|
||||
echo " const marketTaxRateAttributesObserver = new MutationObserver(() => { contentRoot.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation] [name=tax_rate]').forEach((input) => { input.step = '0.01'; input.min = '0'; }); }); marketTaxRateAttributesObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
echo " const marketPaidAmountObserver = new MutationObserver(() => { contentRoot.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation] [name=paid_amount]').forEach((input) => { input.disabled = false; input.removeAttribute('disabled'); input.type = 'number'; input.step = '0.01'; input.min = '0'; input.classList.remove('sg-input-single-line--disabled', 'sg-form-disabled'); }); }); marketPaidAmountObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
echo " const marketPaidAmountPositionObserver = new MutationObserver(() => { contentRoot.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation]').forEach((form) => { const paidGroup = form.querySelector('[name=paid_amount]')?.closest('.sg-form-sections-card__field-group'); const taxGroup = form.querySelector('[name=tax_cost]')?.closest('.sg-form-sections-card__field-group'); if (paidGroup && taxGroup && taxGroup.nextElementSibling !== paidGroup) { taxGroup.after(paidGroup); } }); }); marketPaidAmountPositionObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
echo " const syncMarketSimpleCostTotals = (form) => { const gross = Number.parseFloat(form.querySelector('[name=gross_cost]')?.value || '0'); const rate = form.querySelector('[name=tax_status]')?.value === 'tax_free' ? 0 : Number.parseFloat(form.querySelector('[name=tax_rate]')?.value || '0'); const tax = Number.isFinite(gross) && Number.isFinite(rate) ? Math.round((gross - gross / (1 + rate / 100)) * 100) / 100 : 0; const net = Number.isFinite(gross) ? Math.round((gross - tax) * 100) / 100 : 0; const format = (value) => 'CHF ' + value.toFixed(2); const netField = form.querySelector('[name=net_cost]'); const taxField = form.querySelector('[name=tax_cost]'); if (netField) { netField.value = format(net); } if (taxField) { taxField.value = format(tax); } };";
|
||||
echo " document.addEventListener('input', (event) => { const field = event.target.closest('[data-market-edit-form][data-market-edit-scope=participation] [name=gross_cost], [data-market-edit-form][data-market-edit-scope=participation] [name=tax_rate]'); if (field) { syncMarketSimpleCostTotals(field.closest('[data-market-edit-form]')); } });";
|
||||
echo " document.addEventListener('change', (event) => { const field = event.target.closest('[data-market-edit-form][data-market-edit-scope=participation] [name=tax_status]'); if (field) { syncMarketSimpleCostTotals(field.closest('[data-market-edit-form]')); } });";
|
||||
echo " bindTable();";
|
||||
echo "}";
|
||||
echo "function initMarketDefinitionsBindings() {";
|
||||
echo " const contentRoot = document.querySelector('[data-left-navigation-content-body]');";
|
||||
echo " const definitionContent = contentRoot?.querySelector('[data-market-definitions-content]');";
|
||||
echo " const drawer = definitionContent?.querySelector('[data-market-definition-drawer]');";
|
||||
echo " const readCard = drawer?.querySelector('[data-market-definition-read-card]');";
|
||||
echo " const card = drawer?.querySelector('[data-market-definition-create-card]');";
|
||||
echo " const form = drawer?.querySelector('[data-market-definition-form]');";
|
||||
echo " const editButton = drawer?.querySelector('[data-market-definition-edit]');";
|
||||
echo " if (!contentRoot || !definitionContent || !drawer || !readCard || !card || !form || !editButton || definitionContent.dataset.marketDefinitionsBound === 'true') { return; }";
|
||||
echo " definitionContent.dataset.marketDefinitionsBound = 'true';";
|
||||
echo " let activeMarket = null;";
|
||||
echo " let marketDefinitionMobileHost = null; let marketDefinitionMobileOpen = false;";
|
||||
echo " const marketDefinitionMobileDetail = window.SharedMobileDetail?.create({ contentRoot, bodyClass: 'sg-market-definition-mobile-detail-open', hostClass: 'sg-market-definition-mobile-detail-host', hostDataAttribute: 'data-market-definition-mobile-detail-host', drawerDataAttribute: 'data-market-definition-mobile-drawer', backMarkup: '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\"><div class=\"sg-card-segment sg-card-segment--body\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-market-definition-detail-back>zurück</button></div></div></article></div></div>', historyKey: 'marketDefinitionView' });";
|
||||
echo <<<JS
|
||||
const prepareMarketDefinitionDeleteCard = () => {
|
||||
const existingCard = drawer.querySelector('[data-market-definition-card]');
|
||||
if (!existingCard || drawer.querySelector('[data-market-definition-delete-id]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stage = document.createElement('div');
|
||||
stage.className = 'sg-delete-confirmation-pattern__stage sg-delete-confirmation-pattern__host';
|
||||
stage.dataset.pattern = 'overlay-card';
|
||||
stage.dataset.dialogOpen = 'false';
|
||||
const target = document.createElement('div');
|
||||
target.className = 'sg-delete-confirmation-pattern__target';
|
||||
target.dataset.sharedEditableActionCard = '';
|
||||
target.dataset.sharedEditableDeleteToken = 'DELETE';
|
||||
target.dataset.marketDefinitionDeleteId = '';
|
||||
target.dataset.marketDefinitionCard = '';
|
||||
|
||||
const titleCard = document.createElement('article');
|
||||
titleCard.className = 'sg-card sg-card--content-card market-delete-confirmation-card';
|
||||
const header = document.createElement('header');
|
||||
header.className = 'sg-card-segment sg-card-segment--header sg-card-segment--gray sg-object-card__header';
|
||||
const heading = document.createElement('h2');
|
||||
heading.className = 'sg-heading-h2 market-delete-confirmation-card__title';
|
||||
heading.dataset.marketDefinitionReadTitle = '';
|
||||
heading.dataset.marketDefinitionDrawerTitle = '';
|
||||
heading.textContent = 'Markt';
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'sg-component-row';
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.className = 'sg-interaction-element sg-button sg-button--warning';
|
||||
deleteButton.type = 'button';
|
||||
deleteButton.dataset.sharedEditableCardDelete = '';
|
||||
deleteButton.dataset.marketDefinitionDelete = '';
|
||||
deleteButton.textContent = 'Löschen';
|
||||
actions.append(deleteButton);
|
||||
header.append(heading, actions);
|
||||
titleCard.append(header);
|
||||
existingCard.querySelector('[data-market-definition-read-title]')?.remove();
|
||||
existingCard.removeAttribute('data-shared-editable-action-card');
|
||||
|
||||
const confirmation = document.createElement('article');
|
||||
confirmation.className = 'sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card';
|
||||
confirmation.setAttribute('aria-label', 'Löschbestätigung');
|
||||
confirmation.setAttribute('role', 'dialog');
|
||||
confirmation.setAttribute('aria-modal', 'true');
|
||||
confirmation.dataset.overlayDialog = 'delete';
|
||||
confirmation.dataset.sharedEditableCardDeleteConfirmation = '';
|
||||
confirmation.hidden = true;
|
||||
confirmation.innerHTML = '<div class="sg-card-segment sg-card-segment--body sg-delete-confirmation-pattern__body"><p class="sg-body sg-delete-confirmation-pattern__text"><strong>Möchtest du diesen Markt wirklich löschen?</strong></p><p class="sg-body sg-delete-confirmation-pattern__text">Du kannst das nicht rückgängig machen. Bestätige durch Eingabe von <span class="sg-delete-confirmation-pattern__code">DELETE</span>.</p><label class="sg-labeled-input-row sg-delete-confirmation-pattern__input-row"><span class="sg-label">Bestätigung</span><input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" placeholder="DELETE" aria-label="Löschbestätigung durch DELETE" data-shared-editable-card-delete-input></label><p class="sg-body" data-market-definition-delete-status aria-live="polite"></p><div class="sg-delete-confirmation-pattern__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" data-shared-editable-card-delete-cancel>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process sg-button--process-inactive" type="button" disabled aria-disabled="true" data-shared-editable-card-delete-submit data-market-definition-delete-submit>Löschen</button></div></div>';
|
||||
target.append(titleCard, existingCard, confirmation);
|
||||
stage.append(target);
|
||||
drawer.querySelector('[data-market-definition-drawer-content]')?.append(stage);
|
||||
};
|
||||
JS;
|
||||
echo " prepareMarketDefinitionDeleteCard();";
|
||||
echo " const sharedCard = drawer.querySelector('[data-market-definition-card]');";
|
||||
echo " const websiteInput = form.querySelector('[data-market-definition-website]'); if (websiteInput) { websiteInput.type = 'text'; }";
|
||||
echo " if (window.SharedEditableActionCard) { window.SharedEditableActionCard.init(definitionContent); }";
|
||||
echo " const setDrawerState = (open) => { const drawerOpen = Boolean(open); if (!drawerOpen && marketDefinitionMobileOpen) { marketDefinitionMobileDetail?.restore(); marketDefinitionMobileHost = null; marketDefinitionMobileOpen = false; } const backdropOpen = drawerOpen && !marketDefinitionMobileOpen; drawer.dataset.open = String(drawerOpen); drawer.setAttribute('aria-hidden', String(!drawerOpen)); if (activeMarket?.trigger) { activeMarket.trigger.setAttribute('aria-expanded', String(drawerOpen)); } const backdrop = definitionContent.querySelector('[data-market-definition-drawer-backdrop]'); if (backdrop) { backdrop.dataset.open = String(backdropOpen); backdrop.setAttribute('aria-hidden', String(!backdropOpen)); } document.body.classList.toggle('sg-card-list-page-drawer-open', backdropOpen); };";
|
||||
echo " const restoreMarketDefinitionMobileDetail = () => { marketDefinitionMobileDetail?.restore(); marketDefinitionMobileHost = null; marketDefinitionMobileOpen = false; };";
|
||||
echo " const openMarketDefinitionMobileDetail = (trigger) => { restoreMarketDefinitionMobileDetail(); const sourceDrawer = drawer; const host = document.createElement('div'); host.className = 'sg-market-definition-mobile-detail-host'; host.dataset.marketDefinitionMobileDetailHost = 'true'; sourceDrawer.dataset.marketDefinitionMobileDrawer = 'true'; sourceDrawer.dataset.open = 'true'; sourceDrawer.setAttribute('aria-hidden', 'false'); host.appendChild(sourceDrawer); marketDefinitionMobileHost = host; marketDefinitionMobileOpen = true; marketDefinitionMobileDetail?.mount(sourceDrawer, trigger, host); if (window.SharedEditableActionCard) { window.SharedEditableActionCard.init(host); } showReadCard(trigger); };";
|
||||
echo " const resetForm = () => { form.reset(); const status = form.querySelector('[data-market-definition-status]'); if (status) { status.textContent = ''; } syncNewOrganizerFields(); };";
|
||||
echo " const syncNewOrganizerFields = () => { const select = form.querySelector('[data-market-definition-organizer]'); const fields = form.querySelector('[data-market-definition-new-organizer]'); if (!select || !fields) { return; } fields.hidden = select.value !== '__new__'; };";
|
||||
echo " const closeDrawer = () => { resetForm(); if (sharedCard && window.SharedEditableActionCard) { window.SharedEditableActionCard.resetDeleteConfirmation(sharedCard); window.SharedEditableActionCard.setEditMode(sharedCard, false); } activeMarket = null; setDrawerState(false); };";
|
||||
echo " const deleteMarketDefinition = async (submit) => { if (submit.disabled || !activeMarket?.id) { return; } const card = submit.closest('[data-shared-editable-action-card]'); const input = card?.querySelector('[data-shared-editable-card-delete-input]'); const status = card?.querySelector('[data-market-definition-delete-status]'); if (!card || !input || input.value !== 'DELETE') { return; } if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); try { const response = await fetch('/api/market-definition.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': definitionContent.dataset.marketDefinitionsCsrfToken || '' }, body: JSON.stringify({ action: 'delete', id: activeMarket.id, confirmation: input.value }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Löschen fehlgeschlagen.'); } const row = activeMarket.trigger.closest('[data-market-definition-row]'); row?.remove(); closeDrawer(); } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Löschen fehlgeschlagen.'; } submit.disabled = false; submit.setAttribute('aria-disabled', 'false'); } };";
|
||||
echo " const showReadCard = (trigger) => { activeMarket = { id: trigger.dataset.marketDefinitionId || '', trigger }; drawer.querySelector('[data-market-definition-read-title]').textContent = trigger.dataset.marketDefinitionName || 'Markt'; readCard.querySelector('[data-market-definition-read-name]').textContent = trigger.dataset.marketDefinitionName || 'Nicht hinterlegt'; readCard.querySelector('[data-market-definition-read-category]').textContent = trigger.dataset.marketDefinitionCategoryName || 'Nicht hinterlegt'; readCard.querySelector('[data-market-definition-read-organizer]').textContent = trigger.dataset.marketDefinitionOrganizerName || 'Nicht hinterlegt'; readCard.querySelector('[data-market-definition-read-website]').textContent = trigger.dataset.marketDefinitionWebsite || 'Nicht hinterlegt'; const title = form.querySelector('[data-market-definition-form-title]'); if (title) { title.textContent = 'Markt bearbeiten'; } const name = form.querySelector('[data-market-definition-name]'); const category = form.querySelector('[data-market-definition-category]'); const organizer = form.querySelector('[data-market-definition-organizer]'); const website = form.querySelector('[data-market-definition-website]'); if (name) { name.value = trigger.dataset.marketDefinitionName || ''; } if (category) { category.value = trigger.dataset.marketDefinitionCategoryId || ''; } if (organizer) { organizer.value = trigger.dataset.marketDefinitionOrganizerId || ''; } if (website) { website.value = trigger.dataset.marketDefinitionWebsite === 'Nicht hinterlegt' ? '' : (trigger.dataset.marketDefinitionWebsite || ''); } syncNewOrganizerFields(); if (sharedCard && window.SharedEditableActionCard) { window.SharedEditableActionCard.setEditMode(sharedCard, false); } setDrawerState(true); };";
|
||||
echo " definitionContent.addEventListener('click', (event) => { const trigger = event.target.closest('[data-market-definition-open]'); if (!trigger || !definitionContent.contains(trigger)) { return; } event.preventDefault(); event.stopPropagation(); if (window.matchMedia('(max-width: 767px)').matches) { openMarketDefinitionMobileDetail(trigger); } else { showReadCard(trigger); } }, true);";
|
||||
echo " definitionContent.addEventListener('click', (event) => { const sortButton = event.target.closest('[data-market-definition-sort]'); if (!sortButton || !definitionContent.contains(sortButton)) { return; } event.preventDefault(); const url = new URL(window.location.href); url.searchParams.set('portal_module', 'Märkte'); url.searchParams.set('portal_section', 'Märkte'); url.searchParams.set('market_definition_sort', sortButton.dataset.marketDefinitionSortColumn || 'name'); url.searchParams.set('market_definition_dir', sortButton.dataset.marketDefinitionSortDirection || 'ASC'); window.location.assign(url.toString()); });";
|
||||
echo " definitionContent.addEventListener('click', (event) => { const deleteSubmit = event.target.closest('[data-market-definition-delete-submit]'); if (deleteSubmit) { void deleteMarketDefinition(deleteSubmit); return; } const trigger = event.target.closest('[data-market-definition-open]'); if (trigger) { const deleteButton = drawer.querySelector('[data-market-definition-delete]'); if (deleteButton) { deleteButton.hidden = false; } } if (event.target.closest('[data-market-definition-create]')) { const deleteButton = drawer.querySelector('[data-market-definition-delete]'); if (deleteButton) { deleteButton.hidden = true; } } });";
|
||||
echo " const openEditCard = () => { if (!activeMarket) { return; } const title = form.querySelector('[data-market-definition-form-title]'); if (title) { title.textContent = 'Markt bearbeiten'; } const name = form.querySelector('[data-market-definition-name]'); const category = form.querySelector('[data-market-definition-category]'); const organizer = form.querySelector('[data-market-definition-organizer]'); const website = form.querySelector('[data-market-definition-website]'); if (name) { name.value = activeMarket.trigger.dataset.marketDefinitionName || ''; } if (category) { category.value = activeMarket.trigger.dataset.marketDefinitionCategoryId || ''; } if (organizer) { organizer.value = activeMarket.trigger.dataset.marketDefinitionOrganizerId || ''; } if (website) { website.value = activeMarket.trigger.dataset.marketDefinitionWebsite === 'Nicht hinterlegt' ? '' : (activeMarket.trigger.dataset.marketDefinitionWebsite || ''); } syncNewOrganizerFields(); };";
|
||||
echo " editButton.addEventListener('click', (event) => { event.preventDefault(); openEditCard(); });";
|
||||
echo " form.querySelector('[data-market-definition-organizer]')?.addEventListener('change', syncNewOrganizerFields);";
|
||||
echo " const appendMarketRow = (market) => { const table = contentRoot.querySelector('[data-market-definitions-table]'); if (!table) { return; } const row = document.createElement('div'); row.className = 'sg-card-segment sg-card-segment--body sg-large-table__row'; row.setAttribute('role', 'row'); row.dataset.componentPart = 'large-table-row'; row.dataset.marketDefinitionRow = ''; row.dataset.marketDefinitionId = String(market.id || ''); const category = form.querySelector('[data-market-definition-category]')?.selectedOptions[0]?.textContent || ''; const organizer = form.querySelector('[data-market-definition-organizer]')?.selectedOptions[0]?.textContent || ''; const website = market.website_url || 'Nicht hinterlegt'; const cells = document.createElement('div'); cells.className = 'sg-large-table__cell'; const trigger = document.createElement('a'); trigger.className = 'sg-hyperlink'; trigger.href = '#'; trigger.setAttribute('aria-haspopup', 'dialog'); trigger.setAttribute('aria-expanded', 'false'); trigger.dataset.marketDefinitionOpen = ''; trigger.dataset.marketDefinitionId = String(market.id || ''); trigger.dataset.marketDefinitionName = market.name || ''; trigger.dataset.marketDefinitionCategoryId = String(market.category_id || ''); trigger.dataset.marketDefinitionCategoryName = category; trigger.dataset.marketDefinitionOrganizerId = String(market.organizer_id || ''); trigger.dataset.marketDefinitionOrganizerName = organizer; trigger.dataset.marketDefinitionWebsite = website; trigger.textContent = market.name || ''; cells.appendChild(trigger); row.appendChild(cells); [category, organizer, website, '0', 'Aktiv'].forEach((value) => { const cell = document.createElement('div'); cell.className = 'sg-large-table__cell'; cell.setAttribute('role', 'cell'); cell.textContent = value; row.appendChild(cell); }); table.appendChild(row); };";
|
||||
echo " document.addEventListener('click', (event) => { if (event.target.closest('[data-market-definition-create]')) { event.preventDefault(); activeMarket = null; resetForm(); const title = form.querySelector('[data-market-definition-form-title]'); if (title) { title.textContent = 'Neuen Markt anlegen'; } if (sharedCard && window.SharedEditableActionCard) { window.SharedEditableActionCard.setEditMode(sharedCard, true); } setDrawerState(true); form.querySelector('[data-market-definition-name]')?.focus(); return; } if (event.target.closest('[data-market-definition-edit]')) { event.preventDefault(); openEditCard(); return; } if (event.target.closest('[data-market-definition-cancel]')) { event.preventDefault(); if (!activeMarket) { closeDrawer(); } return; } if (event.target.closest('[data-market-definition-drawer-backdrop]')) { event.preventDefault(); closeDrawer(); } });";
|
||||
echo " document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && drawer.dataset.open === 'true') { closeDrawer(); } });";
|
||||
echo " document.addEventListener('click', (event) => { if (event.target.closest('[data-market-definition-detail-back]')) { event.preventDefault(); if (window.history.state?.marketDefinitionView === 'detail') { window.history.back(); } else { restoreMarketDefinitionMobileDetail(); } } });";
|
||||
echo " window.addEventListener('popstate', (event) => { if (marketDefinitionMobileOpen && (!event.state || event.state.marketDefinitionView !== 'detail')) { restoreMarketDefinitionMobileDetail(); } });";
|
||||
echo " form.addEventListener('submit', async (event) => { event.preventDefault(); const status = form.querySelector('[data-market-definition-status]'); const submit = form.querySelector('[type=submit]'); const fields = Object.fromEntries(new FormData(form).entries()); const isUpdate = Boolean(activeMarket?.id); if (fields.organizer_id === '__new__' && !String(fields.organizer_name || '').trim()) { if (status) { status.textContent = 'Name des neuen Veranstalters ist erforderlich.'; } return; } if (status) { status.textContent = 'Speichern läuft …'; } if (submit) { submit.disabled = true; } try { const response = await fetch('/api/market-definition.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': definitionContent.dataset.marketDefinitionsCsrfToken || '' }, body: JSON.stringify({ action: isUpdate ? 'update' : 'create', id: isUpdate ? activeMarket.id : null, fields }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } if (isUpdate) { const trigger = activeMarket.trigger; const category = form.querySelector('[data-market-definition-category]')?.selectedOptions[0]; const organizer = form.querySelector('[data-market-definition-organizer]')?.selectedOptions[0]; const website = result.market.website_url || 'Nicht hinterlegt'; trigger.textContent = result.market.name; trigger.dataset.marketDefinitionName = result.market.name; trigger.dataset.marketDefinitionCategoryId = String(result.market.category_id); trigger.dataset.marketDefinitionCategoryName = category?.textContent || ''; trigger.dataset.marketDefinitionOrganizerId = String(result.market.organizer_id); trigger.dataset.marketDefinitionOrganizerName = organizer?.textContent || ''; trigger.dataset.marketDefinitionWebsite = website; showReadCard(trigger); } else { appendMarketRow(result.market); closeDrawer(); } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'; } } finally { if (submit) { submit.disabled = false; } } });";
|
||||
echo "}";
|
||||
echo "function initBestellungenBindings() {";
|
||||
echo " const contentRoot = document.querySelector('[data-left-navigation-content-body]');";
|
||||
echo " if (!contentRoot) { return; }";
|
||||
@@ -980,12 +1458,14 @@ 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 formatMarketDay = (value) => value ? new Date(value + 'T00:00:00').toLocaleDateString('de-CH') : '';";
|
||||
echo " const value = (label, content) => '<div class=\"market-detail-row\"><dt>' + escapeHtml(label) + '</dt><dd>' + escapeHtml(content || 'Nicht vorhanden') + '</dd></div>';";
|
||||
echo " const renderCards = (order) => {";
|
||||
echo " const c = order.customer || {}; const a = order.amounts || {}; const address = [[c.street, c.house_number].filter(Boolean).join(' '), [c.zip, c.city].filter(Boolean).join(' '), c.country_name].filter(Boolean).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 " const market = order.market || {}; const marketReference = market.name && market.occurrence_day_date ? market.name + ', Marktdurchführung ' + formatMarketDay(market.occurrence_day_date) : 'kein Marktbezug';";
|
||||
echo " return '<article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Bestellübersicht\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Bestellübersicht</div></header><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\"><dl class=\"market-detail-grid\">' + value('Bestellnummer', order.external_ref) + value('Bestelldatum', formatDate(order.order_date)) + value('Verkaufskanal', order.order_source) + value('Bestellstatus', order.order_status) + value('Zahlungsstatus', order.payment_status) + value('Zahlungsart', order.payment_method) + value('Versandart', order.shipping_method) + value('Kunde', [c.first_name, c.last_name].filter(Boolean).join(' ')) + '<div class=\"market-detail-row\"><dt>Lieferadresse</dt><dd>' + (address || 'Nicht vorhanden') + '</dd></div>' + value('Netto', formatAmount(a.net, a.currency || 'CHF')) + value('Versand', formatAmount(a.shipping, a.currency || 'CHF')) + value('Rabatt', formatAmount(a.discount, a.currency || 'CHF')) + value('Steuer', formatAmount(a.tax, a.currency || 'CHF')) + value('Gesamtsumme', formatAmount(a.total, a.currency || 'CHF')) + value('Markt:', marketReference) + '</dl></div></article><article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Bestellte Artikel\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Bestellte Artikel</div></header>' + (lines || '<div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\"><p class=\"sg-body\">Keine Artikel vorhanden.</p></div>') + '</article><article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Chargenzuordnung\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Chargenzuordnung</div></header><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\">' + (allocations || '<p class=\"sg-body\">Keine Chargenzuordnung vorhanden.</p>') + '</div></article>';";
|
||||
echo " };";
|
||||
echo " setDrawerState(true);";
|
||||
echo " if (drawerTitle) { drawerTitle.textContent = trigger.dataset.orderNumber ? 'Bestellung ' + trigger.dataset.orderNumber : 'Bestellung'; }";
|
||||
@@ -1212,6 +1692,9 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " closeDrawer();";
|
||||
echo " }";
|
||||
echo " });";
|
||||
echo " document.addEventListener('otc-order-created', () => {";
|
||||
echo " void reloadBestellungenSection();";
|
||||
echo " });";
|
||||
echo " window.addEventListener('popstate', (event) => {";
|
||||
echo " if (bestellungenDetailOpen && (!event.state || event.state.bestellungenView !== 'detail')) {";
|
||||
echo " restoreBestellungenList();";
|
||||
@@ -1240,6 +1723,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]');";
|
||||
@@ -1250,6 +1737,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) => {";
|
||||
@@ -1257,6 +1745,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'); }";
|
||||
@@ -1293,6 +1782,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);";
|
||||
@@ -1304,7 +1797,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.';";
|
||||
@@ -1312,6 +1806,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;";
|
||||
@@ -1328,6 +1824,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 = () => {";
|
||||
@@ -1384,7 +1882,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const row = document.createElement('div');";
|
||||
echo " row.className = 'sg-form-sections-card__field-group';";
|
||||
echo " row.dataset.otcPaymentRow = '';";
|
||||
echo " row.innerHTML = '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Bezahlart</span><select class=\"sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable sg-pulldown\" id=\"otc-payment-method-' + number + '\" data-otc-payment-method>' + paymentOptions + '</select></label><label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Betrag (CHF)</span><input class=\"sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable\" type=\"number\" id=\"otc-payment-amount-' + number + '\" min=\"0.01\" step=\"0.01\" value=\"0.00\" data-otc-payment-amount></label>' ;";
|
||||
echo " row.innerHTML = '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Bezahlart</span><select class=\"sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable sg-pulldown\" id=\"otc-payment-method-' + number + '\" data-otc-payment-method>' + paymentOptions + '</select></label><label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Betrag (CHF)</span><input class=\"sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable\" type=\"number\" id=\"otc-payment-amount-' + number + '\" min=\"0.01\" step=\"0.01\" placeholder=\"0.00\" data-otc-payment-amount></label>' ;";
|
||||
echo " row.querySelector('[data-otc-payment-method]').value = nextPaymentMethod;";
|
||||
echo " paymentRows.appendChild(row);";
|
||||
echo " bindPaymentRow(row);";
|
||||
@@ -1394,6 +1892,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;";
|
||||
@@ -1409,6 +1910,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const result = await response.json();";
|
||||
echo " if (response.ok && result.ok) {";
|
||||
echo " submissionSucceeded = true;";
|
||||
echo " document.dispatchEvent(new CustomEvent('otc-order-created', { detail: { orderId: result.orderId } }));";
|
||||
echo " if (successEl) {";
|
||||
echo " successEl.textContent = result.externalRef ? 'Bestellung erfolgreich erfasst! Bestellnummer: ' + result.externalRef : successDefaultText;";
|
||||
echo " successEl.classList.remove('hidden');";
|
||||
@@ -1416,17 +1918,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;";
|
||||
@@ -1443,14 +1943,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 " });";
|
||||
@@ -1458,6 +1960,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) {";
|
||||
@@ -1466,8 +1969,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " outsideClickIgnoreSelectors: ['.sg-sandwich-menu-wrap'],";
|
||||
echo " });";
|
||||
echo "}";
|
||||
echo "renderMainHeading('ERP');";
|
||||
echo "renderLeftNavigation('ERP');";
|
||||
echo "(() => { const url = new URL(window.location.href); const requestedModule = url.searchParams.get('portal_module') || 'ERP'; const moduleButton = Array.from(document.querySelectorAll('.sg-portal-header__tabs .sg-tab-button')).find((button) => button.textContent.trim() === requestedModule) || document.querySelector('.sg-portal-header__tabs .sg-tab-button'); if (!moduleButton) { return; } const moduleName = moduleButton.textContent.trim(); document.querySelectorAll('.sg-portal-header__tabs .sg-tab-button').forEach((button) => { const active = button === moduleButton; button.setAttribute('aria-selected', String(active)); button.dataset.componentState = active ? 'active' : 'inactive'; }); renderMainHeading(moduleName); renderLeftNavigation(moduleName, url.searchParams.get('portal_section') || ''); })();";
|
||||
echo '</script>';
|
||||
echo '</body>';
|
||||
echo '</html>';
|
||||
|
||||
@@ -10,7 +10,7 @@ function throttle_webhook_channel(string $channel, int $minIntervalSeconds): voi
|
||||
$sanitizedChannel = preg_replace('/[^a-z0-9_-]+/i', '_', $channel) ?? 'default';
|
||||
$lockPath = sys_get_temp_dir() . '/erp_naurua_webhook_throttle_' . $sanitizedChannel . '.lock';
|
||||
|
||||
$handle = fopen($lockPath, 'c+');
|
||||
$handle = @fopen($lockPath, 'c+');
|
||||
if ($handle === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ require_once __DIR__ . '/../shared/db.php';
|
||||
require_once __DIR__ . '/../erp/lager/forecast.php';
|
||||
require_once __DIR__ . '/../erp/import-integration/shopify-webhook-worker.php';
|
||||
require_once __DIR__ . '/../erp/import-integration/shopify-delta-run.php';
|
||||
require_once __DIR__ . '/../market_management/service.php';
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
@@ -42,6 +43,7 @@ try {
|
||||
}
|
||||
},
|
||||
'erp.lager.stock_lot_forecast_refresh' => static fn (): array => run_lager_stock_lot_forecast($pdo),
|
||||
'market_management.occurrence_status_completion' => static fn (): array => market_management_auto_complete_occurrences($pdo),
|
||||
];
|
||||
$results = [];
|
||||
foreach ($jobs as $name => $job) {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../modules/shared/db.php';
|
||||
require_once __DIR__ . '/../../modules/erp/lager/mcp.php';
|
||||
|
||||
final class ErpLagerMcpJsonRpcException extends RuntimeException
|
||||
{
|
||||
public function __construct(private int $rpcCode, string $message, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
|
||||
public function rpcCode(): int
|
||||
{
|
||||
return $this->rpcCode;
|
||||
}
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
$env = expand_env_values(parse_env_file(__DIR__ . '/../../.env'));
|
||||
$request = [];
|
||||
|
||||
try {
|
||||
erp_lager_mcp_authorize($env);
|
||||
$pdo = connect_database($env);
|
||||
$body = file_get_contents('php://input');
|
||||
$request = json_decode($body === false ? '' : $body, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($request) || ($request['jsonrpc'] ?? '') !== '2.0') {
|
||||
throw new InvalidArgumentException('Ungültige JSON-RPC-Anfrage.');
|
||||
}
|
||||
|
||||
if (array_key_exists('params', $request) && !is_array($request['params'])) {
|
||||
throw new ErpLagerMcpJsonRpcException(-32602, 'Parameter müssen als Objekt oder Array übergeben werden.');
|
||||
}
|
||||
|
||||
$id = $request['id'] ?? null;
|
||||
$method = (string) ($request['method'] ?? '');
|
||||
$params = is_array($request['params'] ?? null) ? $request['params'] : [];
|
||||
|
||||
if ($method === 'initialize') {
|
||||
$result = [
|
||||
'protocolVersion' => (string) ($params['protocolVersion'] ?? '2025-03-26'),
|
||||
'capabilities' => ['tools' => ['listChanged' => false]],
|
||||
'serverInfo' => ['name' => 'naurua-erp-lager', 'version' => '1.0.0'],
|
||||
];
|
||||
} elseif ($method === 'notifications/initialized') {
|
||||
http_response_code(202);
|
||||
exit;
|
||||
} elseif ($method === 'tools/list') {
|
||||
$result = ['tools' => erp_lager_mcp_tools()];
|
||||
} elseif ($method === 'tools/call') {
|
||||
$tool = (string) ($params['name'] ?? '');
|
||||
$arguments = is_array($params['arguments'] ?? null) ? $params['arguments'] : [];
|
||||
if (array_key_exists('arguments', $params) && !is_array($params['arguments'])) {
|
||||
throw new ErpLagerMcpJsonRpcException(-32602, 'Tool-Argumente müssen als Objekt übergeben werden.');
|
||||
}
|
||||
try {
|
||||
$result = [
|
||||
'content' => [[
|
||||
'type' => 'text',
|
||||
'text' => json_encode(erp_lager_mcp_call($pdo, $tool, $arguments), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
|
||||
]],
|
||||
'isError' => false,
|
||||
];
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new ErpLagerMcpJsonRpcException(-32602, $e->getMessage(), $e);
|
||||
}
|
||||
} else {
|
||||
throw new ErpLagerMcpJsonRpcException(-32601, 'Unbekannte JSON-RPC-Methode: ' . $method);
|
||||
}
|
||||
|
||||
echo json_encode(['jsonrpc' => '2.0', 'id' => $id, 'result' => $result], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
} catch (Throwable $e) {
|
||||
$isAuthError = $e instanceof RuntimeException && str_contains($e->getMessage(), 'Token');
|
||||
$isJsonRpcError = $e instanceof ErpLagerMcpJsonRpcException;
|
||||
http_response_code($isJsonRpcError ? 200 : ($isAuthError ? 401 : 400));
|
||||
echo json_encode([
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => $request['id'] ?? null,
|
||||
'error' => [
|
||||
'code' => $isJsonRpcError ? $e->rpcCode() : -32600,
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../modules/shared/db.php';
|
||||
require_once __DIR__ . '/../../modules/market_management/mcp.php';
|
||||
|
||||
final class MarketManagementMcpJsonRpcException extends RuntimeException
|
||||
{
|
||||
public function __construct(private int $rpcCode, string $message, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
|
||||
public function rpcCode(): int
|
||||
{
|
||||
return $this->rpcCode;
|
||||
}
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
$env = expand_env_values(parse_env_file(__DIR__ . '/../../.env'));
|
||||
|
||||
try {
|
||||
$userSubject = market_management_mcp_authorize($env);
|
||||
$pdo = connect_database($env);
|
||||
$body = file_get_contents('php://input');
|
||||
$request = json_decode($body === false ? '' : $body, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($request) || ($request['jsonrpc'] ?? '') !== '2.0') {
|
||||
throw new InvalidArgumentException('Ungültige JSON-RPC-Anfrage.');
|
||||
}
|
||||
|
||||
if (array_key_exists('params', $request) && !is_array($request['params'])) {
|
||||
throw new MarketManagementMcpJsonRpcException(-32602, 'Parameter müssen als Objekt oder Array übergeben werden.');
|
||||
}
|
||||
|
||||
$id = $request['id'] ?? null;
|
||||
$method = (string) ($request['method'] ?? '');
|
||||
$params = is_array($request['params'] ?? null) ? $request['params'] : [];
|
||||
|
||||
if ($method === 'initialize') {
|
||||
$result = [
|
||||
'protocolVersion' => (string) ($params['protocolVersion'] ?? '2025-03-26'),
|
||||
'capabilities' => ['tools' => ['listChanged' => false]],
|
||||
'serverInfo' => ['name' => 'naurua-market-management', 'version' => '1.0.0'],
|
||||
];
|
||||
} elseif ($method === 'notifications/initialized') {
|
||||
http_response_code(202);
|
||||
exit;
|
||||
} elseif ($method === 'tools/list') {
|
||||
$result = ['tools' => market_management_mcp_tools()];
|
||||
} elseif ($method === 'tools/call') {
|
||||
$tool = (string) ($params['name'] ?? '');
|
||||
$arguments = is_array($params['arguments'] ?? null) ? $params['arguments'] : [];
|
||||
if (array_key_exists('arguments', $params) && !is_array($params['arguments'])) {
|
||||
throw new MarketManagementMcpJsonRpcException(-32602, 'Tool-Argumente müssen als Objekt übergeben werden.');
|
||||
}
|
||||
try {
|
||||
$result = [
|
||||
'content' => [[
|
||||
'type' => 'text',
|
||||
'text' => json_encode(market_management_mcp_call_idempotent($pdo, $userSubject, $tool, $arguments), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
|
||||
]],
|
||||
'isError' => false,
|
||||
];
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new MarketManagementMcpJsonRpcException(-32602, $e->getMessage(), $e);
|
||||
}
|
||||
} else {
|
||||
throw new MarketManagementMcpJsonRpcException(-32601, 'Unbekannte JSON-RPC-Methode: ' . $method);
|
||||
}
|
||||
|
||||
echo json_encode(['jsonrpc' => '2.0', 'id' => $id, 'result' => $result], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
} catch (Throwable $e) {
|
||||
$isAuthError = $e instanceof RuntimeException && str_contains($e->getMessage(), 'Token');
|
||||
$isJsonRpcError = $e instanceof MarketManagementMcpJsonRpcException;
|
||||
http_response_code($isJsonRpcError ? 200 : ($isAuthError ? 401 : 400));
|
||||
echo json_encode([
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => $request['id'] ?? null,
|
||||
'error' => [
|
||||
'code' => $isJsonRpcError ? $e->rpcCode() : -32600,
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
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');
|
||||
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' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$requestMethod = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
||||
if ($requestMethod === 'POST') {
|
||||
$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 {
|
||||
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 (($payload['action'] ?? '') === 'update_checklist') {
|
||||
$checklist = market_management_update_occurrence_checklist($pdo, $id, [
|
||||
'items' => $payload['items'] ?? null,
|
||||
'comment' => $payload['comment'] ?? null,
|
||||
]);
|
||||
echo json_encode(['ok' => true, 'checklist' => $checklist], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
} elseif ($scope === 'day') {
|
||||
market_management_update_day($pdo, $id, $fields);
|
||||
} elseif (in_array($scope, ['occurrence', 'status', 'overview', 'participation', 'infrastructure', 'travel', 'notes'], true)) {
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$occurrenceId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
|
||||
if (!is_int($occurrenceId) || $occurrenceId <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid occurrence id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$occurrence = market_management_get_occurrence($pdo, $occurrenceId);
|
||||
if ($occurrence === null) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['ok' => false, 'error' => 'Occurrence not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$market = market_management_get_market($pdo, (int) $occurrence['market_definition_id']);
|
||||
if ($market === null) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['ok' => false, 'error' => 'Market not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true, 'market' => $market, 'occurrence' => $occurrence], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
@@ -16,6 +16,7 @@
|
||||
--color-process-inactive: #FFAE79;
|
||||
--color-signal-green: #009101;
|
||||
--color-signal-green-light: #65A665;
|
||||
--color-signal-green-red: #C0624D;
|
||||
--color-signal-yellow: #9C7A00;
|
||||
--color-ampel-hellgelb: #D3B962;
|
||||
--color-signal-red: #9B3B2F;
|
||||
@@ -37,6 +38,7 @@
|
||||
--surface-card-header-muted: var(--color-darkbrown);
|
||||
--divider-card-segment: var(--color-white);
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-signal: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-dark);
|
||||
--text-card-transparent: var(--color-font-light);
|
||||
--layout-card-body-content-justify: flex-start;
|
||||
@@ -62,6 +64,7 @@
|
||||
--surface-menu-panel-portal: var(--color-light-grey);
|
||||
--surface-help-icon: var(--color-medium-grey);
|
||||
--surface-help-panel: var(--color-light-grey);
|
||||
--surface-tooltip: var(--color-white);
|
||||
--surface-pulldown-panel: var(--color-light-grey);
|
||||
--surface-activatable-remove: var(--surface-control-default);
|
||||
--surface-slider-track: var(--color-medium-grey);
|
||||
@@ -76,10 +79,8 @@
|
||||
--layout-pulldown-chevron-offset: var(--compact-interaction-padding-horizontal);
|
||||
--layout-tab-navigation-large-padding-inline: var(--interaction-padding-horizontal);
|
||||
--layout-pulldown-chevron-reserved-space: var(--spacing-large);
|
||||
--layout-pulldown-panel-form-mobile-width: 90%;
|
||||
--layer-pulldown-panel: var(--dimension-layer-pulldown-panel);
|
||||
--layout-multiselect-pulldown-panel-desktop-width: var(--dimension-multiselect-pulldown-panel-desktop-width);
|
||||
--layout-multiselect-pulldown-label-column-width-fallback: max-content;
|
||||
--layout-input-label-width: var(--dimension-input-label-width);
|
||||
--layout-input-field-desktop-width: var(--dimension-input-field-desktop-width);
|
||||
--layout-input-field-max-width: var(--dimension-input-field-max-width);
|
||||
@@ -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");
|
||||
@@ -140,7 +144,6 @@
|
||||
--layout-preview-align-items: flex-start;
|
||||
|
||||
/* Semantic pattern tokens: Portal header */
|
||||
--surface-portal-background: var(--color-background-purple);
|
||||
--surface-portal-header: var(--color-darkblue);
|
||||
--surface-portal-header-tab: var(--color-midblue);
|
||||
--surface-portal-header-tab-active: var(--color-light-grey);
|
||||
@@ -306,7 +309,6 @@
|
||||
--dimension-input-label-width: 9rem;
|
||||
--dimension-input-field-desktop-width: 400px;
|
||||
--dimension-input-field-max-width: 600px;
|
||||
--dimension-email-body-max-width: 600px;
|
||||
--dimension-search-field-width: 15.3rem;
|
||||
--dimension-public-content-width: 100%;
|
||||
--dimension-layer-pulldown-panel: 40;
|
||||
@@ -354,7 +356,7 @@ body {
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-base);
|
||||
color: var(--color-font-dark);
|
||||
background: var(--surface-portal-background);
|
||||
background: var(--color-background-purple);
|
||||
margin: 0;
|
||||
padding: var(--spacing-small);
|
||||
}
|
||||
@@ -1760,6 +1762,134 @@ section + section {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel {
|
||||
position: absolute;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
display: block;
|
||||
width: var(--layout-tooltip-width);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: anywhere;
|
||||
padding: var(--spacing-large);
|
||||
border-radius: var(--radius-tooltip);
|
||||
color: var(--text-tooltip);
|
||||
background: var(--surface-tooltip);
|
||||
box-shadow: var(--shadow-overlay);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel {
|
||||
top: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel {
|
||||
bottom: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
right: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
left: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel::before {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel::before {
|
||||
top: calc(var(--spacing-small) * -2);
|
||||
left: 50%;
|
||||
border-width: 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
|
||||
border-color: transparent transparent var(--surface-tooltip);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel::before {
|
||||
bottom: calc(var(--spacing-small) * -2);
|
||||
left: 50%;
|
||||
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
|
||||
border-color: var(--surface-tooltip) transparent transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel::before {
|
||||
top: 50%;
|
||||
right: calc(var(--spacing-small) * -2);
|
||||
border-width: calc(var(--spacing-small) * 2) 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
|
||||
border-color: transparent transparent transparent var(--surface-tooltip);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel::before {
|
||||
top: 50%;
|
||||
left: calc(var(--spacing-small) * -2);
|
||||
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"] {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="bottom"]::before {
|
||||
top: calc(var(--spacing-small) * -2);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
|
||||
border-color: transparent transparent var(--surface-tooltip);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="top"]::before {
|
||||
bottom: calc(var(--spacing-small) * -2);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
|
||||
border-color: var(--surface-tooltip) transparent transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="left"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
right: calc(var(--spacing-small) * -2);
|
||||
border-width: calc(var(--spacing-small) * 2) 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
|
||||
border-color: transparent transparent transparent var(--surface-tooltip);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="right"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
left: calc(var(--spacing-small) * -2);
|
||||
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-pulldown-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + var(--spacing-small));
|
||||
@@ -2248,6 +2378,52 @@ section + section {
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Pattern: History Graph */
|
||||
/* ========================================================= */
|
||||
|
||||
.sg-history-graph {
|
||||
--layout-history-graph-connector-width: var(--score-marker-width);
|
||||
--surface-history-graph-connector: var(--chart-marker-line);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sg-history-graph__event {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__event:not(:last-child)::after {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
width: var(--layout-history-graph-connector-width);
|
||||
height: var(--spacing-large);
|
||||
background: var(--surface-history-graph-connector);
|
||||
content: "";
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-history-graph__card {
|
||||
width: 100%;
|
||||
max-width: var(--layout-object-card-min-width);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.sg-history-graph__navigation {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Patterns: Portal Header */
|
||||
/* ========================================================= */
|
||||
@@ -2449,19 +2625,44 @@ section + section {
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left {
|
||||
order: 0;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-pulldown-demo,
|
||||
.sg-options-row__left > .sg-input-single-line-wrap,
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 1 1 calc(50% - var(--layout-options-row-group-gap));
|
||||
.sg-options-row__left > .sg-pulldown-demo {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left:has(> .sg-pulldown-demo ~ .sg-pulldown-demo) > .sg-pulldown-demo {
|
||||
flex-basis: calc(50% - (var(--layout-options-row-group-gap) / 2));
|
||||
}
|
||||
|
||||
.sg-options-row:not(.sg-options-row--left-only) > .sg-options-row__left:has(> .sg-pulldown-demo:nth-last-child(2):nth-child(odd)) > .sg-pulldown-demo:nth-last-child(2),
|
||||
.sg-options-row:not(.sg-options-row--left-only) > .sg-options-row__left:has(> .sg-pulldown-demo:last-child:nth-child(odd)) > .sg-pulldown-demo:last-child {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left > .sg-pulldown-demo {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 0 0 auto;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row > .sg-search-field-input {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row .sg-input-single-line {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-input-single-line-wrap .sg-input-single-line,
|
||||
@@ -2469,15 +2670,6 @@ section + section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
|
||||
width: min(
|
||||
var(--layout-pulldown-panel-form-mobile-width),
|
||||
calc(100vw - (2 * var(--spacing-large)))
|
||||
);
|
||||
min-width: 0;
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
}
|
||||
|
||||
.sg-options-row .sg-pulldown-panel .sg-form-sections-card-wrapper,
|
||||
.sg-options-row .sg-pulldown-panel .sg-form-sections-card {
|
||||
min-width: 0;
|
||||
@@ -2793,39 +2985,63 @@ section + section {
|
||||
padding-left: var(--layout-form-sections-interaction-indent);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > :not(.sg-form-sections-card__chapter-title) + :not(.sg-form-sections-card__chapter-title) {
|
||||
margin-top: var(--spacing-large);
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] {
|
||||
--layout-multiselect-panel-width: var(--layout-multiselect-pulldown-panel-desktop-width);
|
||||
--layout-multiselect-panel-available-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
--layout-multiselect-chapter-gap: var(--spacing-large);
|
||||
--layout-multiselect-column-gap: var(--spacing-large);
|
||||
--layout-multiselect-control-gap: var(--spacing-small);
|
||||
--layout-multiselect-visual-control-gap: calc(var(--spacing-small) * 2);
|
||||
--layout-multiselect-control-row-height: var(--interaction-height);
|
||||
--layout-multiselect-value-min-width: calc(var(--interaction-height) * 1.25);
|
||||
--typography-multiselect-label-line-height: var(--line-height-base);
|
||||
--opacity-multiselect-disabled: var(--disabled-opacity);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-form-sections-card__chapter-title + * {
|
||||
margin-top: var(--spacing-large);
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > * + * {
|
||||
margin-top: var(--layout-multiselect-chapter-gap);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-slider-row + .sg-slider-row,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-checkbox-field-option + .sg-checkbox-field-option,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-radio-activatable-group + .sg-radio-activatable-group {
|
||||
margin-top: var(--spacing-small);
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-options-row__left > .sg-pulldown-demo {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-pulldown-panel__row + .sg-pulldown-panel__row {
|
||||
margin-top: var(--spacing-large);
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-options-row__left > .sg-pulldown-demo .sg-pulldown {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
|
||||
--sg-multiselect-label-column-width: var(--layout-multiselect-pulldown-label-column-width-fallback);
|
||||
width: min(
|
||||
var(--layout-multiselect-panel-width),
|
||||
var(--layout-multiselect-panel-available-width)
|
||||
);
|
||||
min-width: min(
|
||||
var(--layout-multiselect-panel-width),
|
||||
var(--layout-multiselect-panel-available-width)
|
||||
);
|
||||
max-width: var(--layout-multiselect-panel-available-width);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel .sg-form-sections-card-wrapper,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel .sg-form-sections-card {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] > .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
|
||||
white-space: nowrap;
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] .sg-pulldown-panel__label {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
|
||||
display: grid;
|
||||
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr) auto;
|
||||
grid-template-columns: max-content minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
column-gap: var(--spacing-large);
|
||||
column-gap: var(--layout-multiselect-column-gap);
|
||||
row-gap: var(--layout-multiselect-control-gap);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable {
|
||||
@@ -2833,37 +3049,43 @@ section + section {
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-slider {
|
||||
opacity: var(--disabled-opacity);
|
||||
opacity: var(--opacity-multiselect-disabled);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-slider-value,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-radio-activatable-group__choices {
|
||||
opacity: var(--disabled-opacity);
|
||||
opacity: var(--opacity-multiselect-disabled);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-mode-toggle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider {
|
||||
grid-column: 3;
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 2;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider-value {
|
||||
grid-column: 4;
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
min-width: var(--layout-multiselect-value-min-width);
|
||||
line-height: 1;
|
||||
align-self: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
|
||||
@@ -2872,22 +3094,26 @@ section + section {
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group {
|
||||
display: grid;
|
||||
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr);
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
align-items: center;
|
||||
column-gap: var(--spacing-large);
|
||||
column-gap: var(--layout-multiselect-column-gap);
|
||||
row-gap: var(--layout-multiselect-visual-control-gap);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-mode-toggle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-radio-activatable-group__choices {
|
||||
grid-column: 3;
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-radio-activatable-group__choices {
|
||||
@@ -2896,119 +3122,45 @@ section + section {
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable > :not(.sg-mode-toggle),
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > :not(.sg-mode-toggle),
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > :not(.sg-mode-toggle) {
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__row-header > :not(.sg-mode-toggle),
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] {
|
||||
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr);
|
||||
column-gap: var(--spacing-large);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: var(--layout-multiselect-control-row-height) auto;
|
||||
row-gap: var(--layout-multiselect-visual-control-gap);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-mode-toggle {
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__row-header {
|
||||
display: flex;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
align-items: center;
|
||||
gap: var(--layout-multiselect-column-gap);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
|
||||
grid-column: 2;
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__row-header > .sg-pulldown-panel__label {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
line-height: var(--typography-multiselect-label-line-height);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown {
|
||||
grid-column: 3;
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__row-header > .sg-pulldown-panel__label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown {
|
||||
opacity: var(--disabled-opacity);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-demo .sg-pulldown-panel {
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
|
||||
grid-template-columns: max-content minmax(0, 1fr) auto;
|
||||
row-gap: var(--spacing-small);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] > .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-mode-toggle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider-value {
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group {
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
row-gap: var(--spacing-small);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-mode-toggle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-radio-activatable-group__choices {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] {
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
row-gap: var(--spacing-small);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-mode-toggle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
}
|
||||
opacity: var(--opacity-multiselect-disabled);
|
||||
}
|
||||
|
||||
.sg-form-sections-card__actions {
|
||||
@@ -4215,22 +4367,27 @@ main {
|
||||
|
||||
.sg-card-segment--signal-red {
|
||||
background: var(--color-signal-red);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green {
|
||||
background: var(--color-signal-green);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-light {
|
||||
background: var(--color-signal-green-light);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-red {
|
||||
background: var(--color-signal-green-red);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-yellow {
|
||||
background: var(--color-signal-yellow);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--gray {
|
||||
@@ -4250,6 +4407,11 @@ main {
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card.sg-card--notification-signal-green-red {
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card--content-card {
|
||||
--surface-card-header-primary: var(--surface-content-block-card-title);
|
||||
--surface-card-body: var(--surface-content-block-card-content);
|
||||
@@ -4738,16 +4900,32 @@ main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-large-table[data-column-resize="true"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sg-large-table__viewport {
|
||||
align-self: stretch;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sg-large-table__title-segment {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-large);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sg-large-table__row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
grid-template-columns: var(--large-table-grid-template-columns, repeat(5, minmax(0, 1fr)));
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
@@ -4755,6 +4933,11 @@ main {
|
||||
background: var(--color-white);
|
||||
}
|
||||
|
||||
.sg-large-table[data-column-resize="true"] .sg-large-table__row,
|
||||
.sg-large-table[data-column-resize="true"] .sg-large-table__title-segment {
|
||||
width: max(100%, var(--large-table-grid-min-width, 0px));
|
||||
}
|
||||
|
||||
.sg-large-table__row--hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -4786,16 +4969,59 @@ main {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.sg-large-table__load-more-layout {
|
||||
.sg-large-table__row--load-more,
|
||||
.sg-large-table__cell--load-more,
|
||||
.sg-large-table__load-more-card {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sg-large-table[data-column-resize="true"] .sg-large-table__row--load-more {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.sg-large-table__cell--header {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sg-large-table__resize-handle {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
right: -0.25rem;
|
||||
bottom: 0;
|
||||
width: 0.5rem;
|
||||
cursor: col-resize;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.sg-large-table__resize-handle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
bottom: 25%;
|
||||
left: 50%;
|
||||
width: 0.1rem;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-darkblue);
|
||||
box-shadow: -0.15rem 0 0 var(--color-darkblue), 0.15rem 0 0 var(--color-darkblue);
|
||||
}
|
||||
|
||||
.sg-large-table__resize-handle:hover::after,
|
||||
.sg-large-table__resize-handle:focus-visible::after,
|
||||
.sg-large-table[data-column-resize-state="resizing"] .sg-large-table__resize-handle[data-large-table-resize-active="true"]::after {
|
||||
background: var(--color-darkblue);
|
||||
box-shadow: -0.2rem 0 0 var(--color-darkblue), 0.2rem 0 0 var(--color-darkblue);
|
||||
}
|
||||
|
||||
.sg-large-table__resize-handle:focus-visible {
|
||||
outline: 2px solid var(--color-darkblue);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.sg-large-table__sort-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"styleguideVersion": "2026.05.18.1",
|
||||
"styleguideCommit": "71734d6",
|
||||
"syncedAtUtc": "2026-09-08T09:49:27Z",
|
||||
"styleguideVersion": "2026.05.18.2",
|
||||
"styleguideCommit": "08fc673",
|
||||
"syncedAtUtc": "2026-10-14T11:46:02Z",
|
||||
"sourceRepo": "/Users/mathias/Documents/Dokumente Chouchou/Codebases/Styleguide",
|
||||
"mirroredDocsPath": "docs/styleguide"
|
||||
}
|
||||
+369
-143
@@ -16,6 +16,7 @@
|
||||
--color-process-inactive: #FFAE79;
|
||||
--color-signal-green: #009101;
|
||||
--color-signal-green-light: #65A665;
|
||||
--color-signal-green-red: #C0624D;
|
||||
--color-signal-yellow: #9C7A00;
|
||||
--color-ampel-hellgelb: #D3B962;
|
||||
--color-signal-red: #9B3B2F;
|
||||
@@ -37,6 +38,7 @@
|
||||
--surface-card-header-muted: var(--color-darkbrown);
|
||||
--divider-card-segment: var(--color-white);
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-signal: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-dark);
|
||||
--text-card-transparent: var(--color-font-light);
|
||||
--layout-card-body-content-justify: flex-start;
|
||||
@@ -62,6 +64,7 @@
|
||||
--surface-menu-panel-portal: var(--color-light-grey);
|
||||
--surface-help-icon: var(--color-medium-grey);
|
||||
--surface-help-panel: var(--color-light-grey);
|
||||
--surface-tooltip: var(--color-white);
|
||||
--surface-pulldown-panel: var(--color-light-grey);
|
||||
--surface-activatable-remove: var(--surface-control-default);
|
||||
--surface-slider-track: var(--color-medium-grey);
|
||||
@@ -76,10 +79,8 @@
|
||||
--layout-pulldown-chevron-offset: var(--compact-interaction-padding-horizontal);
|
||||
--layout-tab-navigation-large-padding-inline: var(--interaction-padding-horizontal);
|
||||
--layout-pulldown-chevron-reserved-space: var(--spacing-large);
|
||||
--layout-pulldown-panel-form-mobile-width: 90%;
|
||||
--layer-pulldown-panel: var(--dimension-layer-pulldown-panel);
|
||||
--layout-multiselect-pulldown-panel-desktop-width: var(--dimension-multiselect-pulldown-panel-desktop-width);
|
||||
--layout-multiselect-pulldown-label-column-width-fallback: max-content;
|
||||
--layout-input-label-width: var(--dimension-input-label-width);
|
||||
--layout-input-field-desktop-width: var(--dimension-input-field-desktop-width);
|
||||
--layout-input-field-max-width: var(--dimension-input-field-max-width);
|
||||
@@ -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");
|
||||
@@ -140,7 +144,6 @@
|
||||
--layout-preview-align-items: flex-start;
|
||||
|
||||
/* Semantic pattern tokens: Portal header */
|
||||
--surface-portal-background: var(--color-background-purple);
|
||||
--surface-portal-header: var(--color-darkblue);
|
||||
--surface-portal-header-tab: var(--color-midblue);
|
||||
--surface-portal-header-tab-active: var(--color-light-grey);
|
||||
@@ -306,7 +309,6 @@
|
||||
--dimension-input-label-width: 9rem;
|
||||
--dimension-input-field-desktop-width: 400px;
|
||||
--dimension-input-field-max-width: 600px;
|
||||
--dimension-email-body-max-width: 600px;
|
||||
--dimension-search-field-width: 15.3rem;
|
||||
--dimension-public-content-width: 100%;
|
||||
--dimension-layer-pulldown-panel: 40;
|
||||
@@ -354,7 +356,7 @@ body {
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-base);
|
||||
color: var(--color-font-dark);
|
||||
background: var(--surface-portal-background);
|
||||
background: var(--color-background-purple);
|
||||
margin: 0;
|
||||
padding: var(--spacing-small);
|
||||
}
|
||||
@@ -1760,6 +1762,134 @@ section + section {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel {
|
||||
position: absolute;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
display: block;
|
||||
width: var(--layout-tooltip-width);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: anywhere;
|
||||
padding: var(--spacing-large);
|
||||
border-radius: var(--radius-tooltip);
|
||||
color: var(--text-tooltip);
|
||||
background: var(--surface-tooltip);
|
||||
box-shadow: var(--shadow-overlay);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel {
|
||||
top: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel {
|
||||
bottom: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
right: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
left: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel::before {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel::before {
|
||||
top: calc(var(--spacing-small) * -2);
|
||||
left: 50%;
|
||||
border-width: 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
|
||||
border-color: transparent transparent var(--surface-tooltip);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel::before {
|
||||
bottom: calc(var(--spacing-small) * -2);
|
||||
left: 50%;
|
||||
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
|
||||
border-color: var(--surface-tooltip) transparent transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel::before {
|
||||
top: 50%;
|
||||
right: calc(var(--spacing-small) * -2);
|
||||
border-width: calc(var(--spacing-small) * 2) 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
|
||||
border-color: transparent transparent transparent var(--surface-tooltip);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel::before {
|
||||
top: 50%;
|
||||
left: calc(var(--spacing-small) * -2);
|
||||
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"] {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="bottom"]::before {
|
||||
top: calc(var(--spacing-small) * -2);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
|
||||
border-color: transparent transparent var(--surface-tooltip);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="top"]::before {
|
||||
bottom: calc(var(--spacing-small) * -2);
|
||||
left: var(--tooltip-arrow-offset);
|
||||
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
|
||||
border-color: var(--surface-tooltip) transparent transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="left"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
right: calc(var(--spacing-small) * -2);
|
||||
border-width: calc(var(--spacing-small) * 2) 0 calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2);
|
||||
border-color: transparent transparent transparent var(--surface-tooltip);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[data-tooltip-portal="true"][data-tooltip-placement="right"]::before {
|
||||
top: var(--tooltip-arrow-offset);
|
||||
left: calc(var(--spacing-small) * -2);
|
||||
border-width: calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) calc(var(--spacing-small) * 2) 0;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-pulldown-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + var(--spacing-small));
|
||||
@@ -2248,6 +2378,52 @@ section + section {
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Pattern: History Graph */
|
||||
/* ========================================================= */
|
||||
|
||||
.sg-history-graph {
|
||||
--layout-history-graph-connector-width: var(--score-marker-width);
|
||||
--surface-history-graph-connector: var(--chart-marker-line);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sg-history-graph__event {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-history-graph__event:not(:last-child)::after {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
width: var(--layout-history-graph-connector-width);
|
||||
height: var(--spacing-large);
|
||||
background: var(--surface-history-graph-connector);
|
||||
content: "";
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-history-graph__card {
|
||||
width: 100%;
|
||||
max-width: var(--layout-object-card-min-width);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.sg-history-graph__navigation {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Patterns: Portal Header */
|
||||
/* ========================================================= */
|
||||
@@ -2449,19 +2625,44 @@ section + section {
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left {
|
||||
order: 0;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-pulldown-demo,
|
||||
.sg-options-row__left > .sg-input-single-line-wrap,
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 1 1 calc(50% - var(--layout-options-row-group-gap));
|
||||
.sg-options-row__left > .sg-pulldown-demo {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left:has(> .sg-pulldown-demo ~ .sg-pulldown-demo) > .sg-pulldown-demo {
|
||||
flex-basis: calc(50% - (var(--layout-options-row-group-gap) / 2));
|
||||
}
|
||||
|
||||
.sg-options-row:not(.sg-options-row--left-only) > .sg-options-row__left:has(> .sg-pulldown-demo:nth-last-child(2):nth-child(odd)) > .sg-pulldown-demo:nth-last-child(2),
|
||||
.sg-options-row:not(.sg-options-row--left-only) > .sg-options-row__left:has(> .sg-pulldown-demo:last-child:nth-child(odd)) > .sg-pulldown-demo:last-child {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row--left-only .sg-options-row__left > .sg-pulldown-demo {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row {
|
||||
flex: 0 0 auto;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row > .sg-search-field-input {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-search-field-row .sg-input-single-line {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row__left > .sg-input-single-line-wrap .sg-input-single-line,
|
||||
@@ -2469,15 +2670,6 @@ section + section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
|
||||
width: min(
|
||||
var(--layout-pulldown-panel-form-mobile-width),
|
||||
calc(100vw - (2 * var(--spacing-large)))
|
||||
);
|
||||
min-width: 0;
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
}
|
||||
|
||||
.sg-options-row .sg-pulldown-panel .sg-form-sections-card-wrapper,
|
||||
.sg-options-row .sg-pulldown-panel .sg-form-sections-card {
|
||||
min-width: 0;
|
||||
@@ -2793,39 +2985,63 @@ section + section {
|
||||
padding-left: var(--layout-form-sections-interaction-indent);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > :not(.sg-form-sections-card__chapter-title) + :not(.sg-form-sections-card__chapter-title) {
|
||||
margin-top: var(--spacing-large);
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] {
|
||||
--layout-multiselect-panel-width: var(--layout-multiselect-pulldown-panel-desktop-width);
|
||||
--layout-multiselect-panel-available-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
--layout-multiselect-chapter-gap: var(--spacing-large);
|
||||
--layout-multiselect-column-gap: var(--spacing-large);
|
||||
--layout-multiselect-control-gap: var(--spacing-small);
|
||||
--layout-multiselect-visual-control-gap: calc(var(--spacing-small) * 2);
|
||||
--layout-multiselect-control-row-height: var(--interaction-height);
|
||||
--layout-multiselect-value-min-width: calc(var(--interaction-height) * 1.25);
|
||||
--typography-multiselect-label-line-height: var(--line-height-base);
|
||||
--opacity-multiselect-disabled: var(--disabled-opacity);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-form-sections-card__chapter-title + * {
|
||||
margin-top: var(--spacing-large);
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > * + * {
|
||||
margin-top: var(--layout-multiselect-chapter-gap);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-slider-row + .sg-slider-row,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-checkbox-field-option + .sg-checkbox-field-option,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-radio-activatable-group + .sg-radio-activatable-group {
|
||||
margin-top: var(--spacing-small);
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-options-row__left > .sg-pulldown-demo {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-form-sections-card__chapter > .sg-pulldown-panel__row + .sg-pulldown-panel__row {
|
||||
margin-top: var(--spacing-large);
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-options-row__left > .sg-pulldown-demo .sg-pulldown {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
|
||||
--sg-multiselect-label-column-width: var(--layout-multiselect-pulldown-label-column-width-fallback);
|
||||
width: min(
|
||||
var(--layout-multiselect-panel-width),
|
||||
var(--layout-multiselect-panel-available-width)
|
||||
);
|
||||
min-width: min(
|
||||
var(--layout-multiselect-panel-width),
|
||||
var(--layout-multiselect-panel-available-width)
|
||||
);
|
||||
max-width: var(--layout-multiselect-panel-available-width);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel .sg-form-sections-card-wrapper,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel .sg-form-sections-card {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] > .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
|
||||
white-space: nowrap;
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] .sg-pulldown-panel__label {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
|
||||
display: grid;
|
||||
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr) auto;
|
||||
grid-template-columns: max-content minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
column-gap: var(--spacing-large);
|
||||
column-gap: var(--layout-multiselect-column-gap);
|
||||
row-gap: var(--layout-multiselect-control-gap);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable {
|
||||
@@ -2833,37 +3049,43 @@ section + section {
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-slider {
|
||||
opacity: var(--disabled-opacity);
|
||||
opacity: var(--opacity-multiselect-disabled);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable .sg-slider-value,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-radio-activatable-group__choices {
|
||||
opacity: var(--disabled-opacity);
|
||||
opacity: var(--opacity-multiselect-disabled);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-mode-toggle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider {
|
||||
grid-column: 3;
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 2;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider-value {
|
||||
grid-column: 4;
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
min-width: var(--layout-multiselect-value-min-width);
|
||||
line-height: 1;
|
||||
align-self: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
|
||||
@@ -2872,22 +3094,26 @@ section + section {
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group {
|
||||
display: grid;
|
||||
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr);
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
align-items: center;
|
||||
column-gap: var(--spacing-large);
|
||||
column-gap: var(--layout-multiselect-column-gap);
|
||||
row-gap: var(--layout-multiselect-visual-control-gap);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-mode-toggle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-radio-activatable-group__choices {
|
||||
grid-column: 3;
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > .sg-radio-activatable-group__choices {
|
||||
@@ -2896,119 +3122,45 @@ section + section {
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"].sg-slider-row--inactive-selectable > :not(.sg-mode-toggle),
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group[data-component-state="inactive-selectable"] > :not(.sg-mode-toggle),
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > :not(.sg-mode-toggle) {
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__row-header > :not(.sg-mode-toggle),
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] {
|
||||
grid-template-columns: max-content var(--sg-multiselect-label-column-width) minmax(0, 1fr);
|
||||
column-gap: var(--spacing-large);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: var(--layout-multiselect-control-row-height) auto;
|
||||
row-gap: var(--layout-multiselect-visual-control-gap);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-mode-toggle {
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__row-header {
|
||||
display: flex;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
align-items: center;
|
||||
gap: var(--layout-multiselect-column-gap);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
|
||||
grid-column: 2;
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__row-header > .sg-pulldown-panel__label {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
line-height: var(--typography-multiselect-label-line-height);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown {
|
||||
grid-column: 3;
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown-panel__row-header > .sg-pulldown-panel__label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row].sg-pulldown-panel__row--disabled > .sg-pulldown {
|
||||
opacity: var(--disabled-opacity);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-demo .sg-pulldown-panel {
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-pulldown-panel {
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] {
|
||||
grid-template-columns: max-content minmax(0, 1fr) auto;
|
||||
row-gap: var(--spacing-small);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] > .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label,
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-mode-toggle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-slider-row[data-activatable="true"] .sg-slider-value {
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group {
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
row-gap: var(--spacing-small);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-mode-toggle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] .sg-radio-activatable-group > .sg-radio-activatable-group__choices {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] {
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
row-gap: var(--spacing-small);
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-mode-toggle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown-panel__label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.sg-options-row[data-pattern="multiselektions-pulldown"] [data-pulldown-filter-row] > .sg-pulldown {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
}
|
||||
opacity: var(--opacity-multiselect-disabled);
|
||||
}
|
||||
|
||||
.sg-form-sections-card__actions {
|
||||
@@ -4215,22 +4367,27 @@ main {
|
||||
|
||||
.sg-card-segment--signal-red {
|
||||
background: var(--color-signal-red);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green {
|
||||
background: var(--color-signal-green);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-light {
|
||||
background: var(--color-signal-green-light);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-green-red {
|
||||
background: var(--color-signal-green-red);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--signal-yellow {
|
||||
background: var(--color-signal-yellow);
|
||||
color: var(--text-card-header);
|
||||
color: var(--text-card-signal);
|
||||
}
|
||||
|
||||
.sg-card-segment--gray {
|
||||
@@ -4250,6 +4407,11 @@ main {
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card.sg-card--notification-signal-green-red {
|
||||
--text-card-header: var(--color-font-light);
|
||||
--text-card-body: var(--color-font-light);
|
||||
}
|
||||
|
||||
.sg-card--content-card {
|
||||
--surface-card-header-primary: var(--surface-content-block-card-title);
|
||||
--surface-card-body: var(--surface-content-block-card-content);
|
||||
@@ -4738,16 +4900,32 @@ main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-large-table[data-column-resize="true"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sg-large-table__viewport {
|
||||
align-self: stretch;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sg-large-table__title-segment {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-large);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sg-large-table__row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
grid-template-columns: var(--large-table-grid-template-columns, repeat(5, minmax(0, 1fr)));
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
@@ -4755,6 +4933,11 @@ main {
|
||||
background: var(--color-white);
|
||||
}
|
||||
|
||||
.sg-large-table[data-column-resize="true"] .sg-large-table__row,
|
||||
.sg-large-table[data-column-resize="true"] .sg-large-table__title-segment {
|
||||
width: max(100%, var(--large-table-grid-min-width, 0px));
|
||||
}
|
||||
|
||||
.sg-large-table__row--hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -4786,16 +4969,59 @@ main {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.sg-large-table__load-more-layout {
|
||||
.sg-large-table__row--load-more,
|
||||
.sg-large-table__cell--load-more,
|
||||
.sg-large-table__load-more-card {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sg-large-table[data-column-resize="true"] .sg-large-table__row--load-more {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.sg-large-table__cell--header {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sg-large-table__resize-handle {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
right: -0.25rem;
|
||||
bottom: 0;
|
||||
width: 0.5rem;
|
||||
cursor: col-resize;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.sg-large-table__resize-handle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
bottom: 25%;
|
||||
left: 50%;
|
||||
width: 0.1rem;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-darkblue);
|
||||
box-shadow: -0.15rem 0 0 var(--color-darkblue), 0.15rem 0 0 var(--color-darkblue);
|
||||
}
|
||||
|
||||
.sg-large-table__resize-handle:hover::after,
|
||||
.sg-large-table__resize-handle:focus-visible::after,
|
||||
.sg-large-table[data-column-resize-state="resizing"] .sg-large-table__resize-handle[data-large-table-resize-active="true"]::after {
|
||||
background: var(--color-darkblue);
|
||||
box-shadow: -0.2rem 0 0 var(--color-darkblue), 0.2rem 0 0 var(--color-darkblue);
|
||||
}
|
||||
|
||||
.sg-large-table__resize-handle:focus-visible {
|
||||
outline: 2px solid var(--color-darkblue);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.sg-large-table__sort-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -39,6 +39,102 @@ body.sg-card-list-page-drawer-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sg-market-mobile-detail-host {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1500;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
padding: var(--spacing-small);
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
background: var(--color-background-purple);
|
||||
}
|
||||
|
||||
.sg-market-definition-mobile-detail-host {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1500;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
padding: var(--spacing-small);
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
background: var(--color-background-purple);
|
||||
}
|
||||
|
||||
.sg-market-mobile-detail-host .sg-card-list-page-drawer {
|
||||
position: static;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.sg-market-definition-mobile-detail-host .sg-card-list-page-drawer {
|
||||
position: static;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
body.sg-market-mobile-detail-open > :not([data-market-mobile-detail-host]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.sg-market-definition-mobile-detail-open > :not([data-market-definition-mobile-detail-host]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.market-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(10rem, 0.8fr) minmax(0, 1.2fr);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.market-detail-row {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.market-detail-row dt,
|
||||
.market-detail-row dd {
|
||||
margin: 0;
|
||||
padding: var(--spacing-small) 0;
|
||||
border-bottom: 1px solid var(--color-light-grey);
|
||||
}
|
||||
|
||||
.market-detail-row dt {
|
||||
color: var(--color-black);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.market-detail-row dd {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
.sg-card-list-page-drawer-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -207,7 +303,7 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
|
||||
}
|
||||
|
||||
.sg-large-table--bestellungen .sg-large-table__row {
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1.35fr) fit-content(12.5ch);
|
||||
grid-template-columns: var(--large-table-grid-template-columns, minmax(0, 1.15fr) minmax(0, 1.35fr) fit-content(12.5ch));
|
||||
}
|
||||
|
||||
.sg-large-table--bestellungen .sg-large-table__row > :nth-child(2) {
|
||||
@@ -220,7 +316,7 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
|
||||
}
|
||||
|
||||
.sg-large-table--bestellungen .sg-large-table__row {
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1.35fr) fit-content(12.5ch);
|
||||
grid-template-columns: var(--large-table-grid-template-columns, minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1.35fr) fit-content(12.5ch));
|
||||
}
|
||||
|
||||
.sg-large-table--bestellungen .sg-large-table__cell--amount {
|
||||
@@ -243,11 +339,15 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
|
||||
}
|
||||
|
||||
.sg-large-table--lager .sg-large-table__row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: var(--large-table-grid-template-columns, repeat(2, minmax(0, 1fr)));
|
||||
}
|
||||
|
||||
.sg-large-table--markets .sg-large-table__row {
|
||||
grid-template-columns: var(--large-table-grid-template-columns, repeat(6, minmax(0, 1fr)));
|
||||
}
|
||||
|
||||
.sg-large-table--lager[data-lager-table-key="products"] .sg-large-table__row {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
grid-template-columns: var(--large-table-grid-template-columns, 30% repeat(4, minmax(0, 1fr)));
|
||||
}
|
||||
|
||||
.sg-large-table--lager[data-lager-table-key="products"] .sg-large-table__cell:last-child {
|
||||
@@ -270,15 +370,142 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
|
||||
color: var(--color-signal-yellow);
|
||||
}
|
||||
|
||||
.sg-large-table--markets .market-status--red {
|
||||
color: var(--color-signal-red);
|
||||
}
|
||||
|
||||
.sg-large-table--markets .market-status--yellow {
|
||||
color: var(--color-signal-yellow);
|
||||
}
|
||||
|
||||
.sg-large-table--markets .market-status--green {
|
||||
color: var(--color-signal-green);
|
||||
}
|
||||
|
||||
.sg-card-list-page-drawer [data-lager-edit-form][hidden],
|
||||
.sg-card-list-page-drawer [data-lager-read-view][hidden],
|
||||
.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-checklist-list,
|
||||
.market-checklist-edit-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-small);
|
||||
}
|
||||
|
||||
.market-checklist-item,
|
||||
.market-checklist-edit-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-small);
|
||||
}
|
||||
|
||||
.market-checklist-item .sg-checkbox-field,
|
||||
.market-checklist-edit-row .sg-checkbox-field {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.market-checklist-edit-item [data-checklist-label] {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.market-checklist-edit-row .sg-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.market-checklist-edit-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-small);
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: var(--layout-form-input-field-max-width);
|
||||
}
|
||||
|
||||
.market-checklist-edit-control [data-checklist-label] {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.market-checklist-comment-field .sg-input-multi-line {
|
||||
width: 100%;
|
||||
max-width: var(--layout-form-input-field-max-width);
|
||||
}
|
||||
|
||||
.market-checklist-delete {
|
||||
min-width: var(--small-interaction-element-size);
|
||||
width: var(--small-interaction-element-size);
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.market-checklist-comment,
|
||||
.market-checklist-comment-field {
|
||||
margin-top: var(--spacing-large);
|
||||
}
|
||||
|
||||
.market-required-field-indicator {
|
||||
position: relative;
|
||||
top: -0.25em;
|
||||
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;
|
||||
}
|
||||
|
||||
.sg-card-list-page-drawer [data-lager-drawer-topic="new-lot-action"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -293,3 +520,22 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
|
||||
[data-bestellungen-mobile-detail] .sg-navigation-card-layout {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.market-checklist-edit-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-small);
|
||||
}
|
||||
|
||||
.market-checklist-edit-row .sg-label {
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.market-checklist-edit-control {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
+41
-1
@@ -8,6 +8,7 @@ require_once __DIR__ . '/../modules/erp/bestellungen/service.php';
|
||||
require_once __DIR__ . '/../modules/erp/lager/service.php';
|
||||
require_once __DIR__ . '/../modules/erp/kontakte/service.php';
|
||||
require_once __DIR__ . '/../modules/erp/artikel-mapping/service.php';
|
||||
require_once __DIR__ . '/../modules/market_management/service.php';
|
||||
|
||||
$env = expand_env_values(parse_env_file(__DIR__ . '/../.env'));
|
||||
$pdo = connect_database($env);
|
||||
@@ -48,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);
|
||||
@@ -58,11 +60,49 @@ if ($currentUser !== null) {
|
||||
'limit' => (int) ($_GET['bestellungen_limit'] ?? 20),
|
||||
'page_size' => 20,
|
||||
]);
|
||||
$marketSearch = (string) ($_GET['market_search'] ?? '');
|
||||
$marketSort = (string) ($_GET['market_sort'] ?? 'earliest_date');
|
||||
$marketDirection = strtoupper((string) ($_GET['market_dir'] ?? 'ASC'));
|
||||
$marketLimit = min(100, max(1, (int) ($_GET['market_limit'] ?? 20)));
|
||||
$marketOccurrencesTable = [
|
||||
'rows' => market_management_search_occurrences($pdo, [
|
||||
'search' => $marketSearch,
|
||||
'sort' => $marketSort,
|
||||
'sort_direction' => $marketDirection,
|
||||
'limit' => $marketLimit,
|
||||
'offset' => 0,
|
||||
]),
|
||||
'search' => $marketSearch,
|
||||
'sort_column' => $marketSort,
|
||||
'sort_direction' => $marketDirection,
|
||||
'limit' => $marketLimit,
|
||||
'page_size' => 20,
|
||||
];
|
||||
$marketOccurrencesTable['has_more'] = count($marketOccurrencesTable['rows']) >= $marketLimit;
|
||||
$marketDefinitionSort = (string) ($_GET['market_definition_sort'] ?? 'name');
|
||||
$marketDefinitionDirection = strtoupper((string) ($_GET['market_definition_dir'] ?? 'ASC'));
|
||||
$marketDefinitionsTable = [
|
||||
'rows' => market_management_search_markets($pdo, [
|
||||
'include_archived' => false,
|
||||
'sort' => $marketDefinitionSort,
|
||||
'sort_direction' => $marketDefinitionDirection,
|
||||
'limit' => 100,
|
||||
'offset' => 0,
|
||||
]),
|
||||
'categories' => market_management_list_categories($pdo),
|
||||
'organizers' => market_management_list_organizers($pdo),
|
||||
'sort_column' => $marketDefinitionSort,
|
||||
'sort_direction' => $marketDefinitionDirection,
|
||||
];
|
||||
if ((string) ($_GET['bestellungen_fragment'] ?? '') === '1') {
|
||||
echo auth_render_bestellungen_content($bestellungenTable);
|
||||
exit;
|
||||
}
|
||||
render_auth_home_page($currentUser, $otcProducts, $bestellungenTable, $lagerProducts, $lagerArtikel, $lagerManufacturers);
|
||||
if ((string) ($_GET['market_fragment'] ?? '') === '1') {
|
||||
echo auth_render_markets_content($marketOccurrencesTable);
|
||||
exit;
|
||||
}
|
||||
render_auth_home_page($currentUser, $otcProducts, $otcMarketOptions, $bestellungenTable, $marketOccurrencesTable, $lagerProducts, $lagerArtikel, $lagerManufacturers, $marketDefinitionsTable);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
(function initLargeTableColumnResizeModule() {
|
||||
const STORAGE_PREFIX = 'sg:large-table:column-widths:';
|
||||
const STORAGE_VERSION = 3;
|
||||
const DEFAULT_MIN_WIDTH = 128;
|
||||
const KEYBOARD_STEP = 8;
|
||||
const KEYBOARD_LARGE_STEP = 40;
|
||||
let activeResize = null;
|
||||
|
||||
const getTables = (root) => root.querySelectorAll('[data-component="large-table"][data-column-resize="true"]');
|
||||
|
||||
const getColumnHeaders = (table) => Array.from(
|
||||
table.querySelectorAll('[data-component-part="large-table-header-row"] [role="columnheader"]'),
|
||||
);
|
||||
|
||||
const getMinWidth = (table) => {
|
||||
const configured = table.dataset.columnMinWidth;
|
||||
const value = configured || getComputedStyle(table).getPropertyValue('--large-table-column-min-width').trim();
|
||||
if (!value) {
|
||||
return DEFAULT_MIN_WIDTH;
|
||||
}
|
||||
|
||||
const probe = document.createElement('span');
|
||||
probe.style.cssText = `position:absolute;width:${value};visibility:hidden;`;
|
||||
table.appendChild(probe);
|
||||
const width = probe.getBoundingClientRect().width;
|
||||
probe.remove();
|
||||
return width > 0 ? width : DEFAULT_MIN_WIDTH;
|
||||
};
|
||||
|
||||
const parseDefaultWidth = (rawValue, tableWidth) => {
|
||||
if (!rawValue) {
|
||||
return null;
|
||||
}
|
||||
const value = Number.parseFloat(rawValue);
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (rawValue.trim().endsWith('%')) {
|
||||
return tableWidth * value / 100;
|
||||
}
|
||||
if (rawValue.trim().endsWith('fr')) {
|
||||
return value;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const normalizeRatios = (values) => {
|
||||
const safeValues = values.map((value) => (Number.isFinite(value) && value > 0 ? value : 0));
|
||||
const total = safeValues.reduce((sum, value) => sum + value, 0);
|
||||
if (total <= 0) {
|
||||
return Array(values.length).fill(1 / values.length);
|
||||
}
|
||||
return safeValues.map((value) => value / total);
|
||||
};
|
||||
|
||||
const getDefaultRatios = (table, headers, minWidth) => {
|
||||
const tableWidth = Math.max(table.querySelector('[data-component-part="large-table-viewport"]')?.clientWidth || 0, minWidth * headers.length);
|
||||
const configured = headers.map((header) => parseDefaultWidth(header.dataset.defaultWidth, tableWidth));
|
||||
if (configured.some((width) => width === null)) {
|
||||
return Array(headers.length).fill(1 / headers.length);
|
||||
}
|
||||
return normalizeRatios(configured);
|
||||
};
|
||||
|
||||
const getStorageKey = (table) => `${STORAGE_PREFIX}${table.dataset.columnResizeKey || ''}`;
|
||||
|
||||
const readStoredRatios = (table, count) => {
|
||||
const key = getStorageKey(table);
|
||||
if (!table.dataset.columnResizeKey) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (!window.localStorage) {
|
||||
return null;
|
||||
}
|
||||
const stored = JSON.parse(window.localStorage.getItem(key));
|
||||
if (stored?.version !== STORAGE_VERSION || !Array.isArray(stored.ratios) || stored.ratios.length !== count) {
|
||||
return null;
|
||||
}
|
||||
if (stored.ratios.some((ratio) => !Number.isFinite(ratio) || ratio <= 0)) {
|
||||
return null;
|
||||
}
|
||||
return normalizeRatios(stored.ratios);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const writeStoredRatios = (table, ratios) => {
|
||||
if (!table.dataset.columnResizeKey) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!window.localStorage) {
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(getStorageKey(table), JSON.stringify({
|
||||
version: STORAGE_VERSION,
|
||||
ratios: normalizeRatios(ratios),
|
||||
}));
|
||||
} catch (error) {
|
||||
// Storage can be unavailable in privacy-restricted browser contexts.
|
||||
}
|
||||
};
|
||||
|
||||
const updateHandleState = (table, widths, minWidth) => {
|
||||
const handles = table.querySelectorAll('[data-large-table-resize-handle]');
|
||||
handles.forEach((handle) => {
|
||||
const index = Number.parseInt(handle.dataset.largeTableResizeHandle, 10);
|
||||
const current = widths[index];
|
||||
const maximum = Math.max(minWidth, widths[index] + widths[index + 1] - minWidth);
|
||||
if (table.id) {
|
||||
handle.setAttribute('aria-controls', table.id);
|
||||
}
|
||||
handle.setAttribute('aria-valuemin', String(Math.round(minWidth)));
|
||||
handle.setAttribute('aria-valuemax', String(Math.round(maximum)));
|
||||
handle.setAttribute('aria-valuenow', String(Math.round(current)));
|
||||
handle.setAttribute('aria-valuetext', `${Math.round(current)} Pixel`);
|
||||
});
|
||||
};
|
||||
|
||||
const getRenderedWidths = (table, count, minWidth) => {
|
||||
const headers = getColumnHeaders(table);
|
||||
const widths = headers.map((header) => header.getBoundingClientRect().width);
|
||||
if (widths.length === count && widths.every((width) => width > 0)) {
|
||||
return widths;
|
||||
}
|
||||
return Array(count).fill(minWidth);
|
||||
};
|
||||
|
||||
const applyRatios = (table, ratios, minWidth) => {
|
||||
const safeRatios = normalizeRatios(ratios);
|
||||
const columnTemplate = safeRatios
|
||||
.map((ratio) => `minmax(${Math.ceil(minWidth)}px, ${ratio}fr)`)
|
||||
.join(' ');
|
||||
table.style.setProperty('--large-table-grid-template-columns', columnTemplate);
|
||||
table.style.setProperty('--large-table-grid-min-width', `${Math.ceil(minWidth * safeRatios.length)}px`);
|
||||
table._largeTableColumnRatios = safeRatios;
|
||||
table._largeTableColumnWidths = getRenderedWidths(table, safeRatios.length, minWidth);
|
||||
updateHandleState(table, table._largeTableColumnWidths, minWidth);
|
||||
};
|
||||
|
||||
const resizeColumn = (table, index, delta, persist, baseWidths = table._largeTableColumnWidths) => {
|
||||
const widths = [...baseWidths];
|
||||
const minWidth = getMinWidth(table);
|
||||
const nextLeft = Math.max(minWidth, Math.min(widths[index] + widths[index + 1] - minWidth, widths[index] + delta));
|
||||
const appliedDelta = nextLeft - widths[index];
|
||||
widths[index] = nextLeft;
|
||||
widths[index + 1] -= appliedDelta;
|
||||
applyRatios(table, widths.map((width) => width / widths.reduce((sum, value) => sum + value, 0)), minWidth);
|
||||
if (persist) {
|
||||
writeStoredRatios(table, table._largeTableColumnRatios);
|
||||
}
|
||||
};
|
||||
|
||||
const initTable = (table) => {
|
||||
const headers = getColumnHeaders(table);
|
||||
if (headers.length < 2) {
|
||||
return;
|
||||
}
|
||||
const minWidth = getMinWidth(table);
|
||||
const stored = readStoredRatios(table, headers.length);
|
||||
applyRatios(table, stored || getDefaultRatios(table, headers, minWidth), minWidth);
|
||||
|
||||
if (table.dataset.largeTableResizeInitialized === 'true') {
|
||||
return;
|
||||
}
|
||||
table.dataset.largeTableResizeInitialized = 'true';
|
||||
|
||||
table.addEventListener('pointerdown', (event) => {
|
||||
const handle = event.target.closest('[data-large-table-resize-handle]');
|
||||
if (!handle || !table.contains(handle)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
activeResize = {
|
||||
table,
|
||||
handle,
|
||||
index: Number.parseInt(handle.dataset.largeTableResizeHandle, 10),
|
||||
startX: event.clientX,
|
||||
startWidths: [...table._largeTableColumnWidths],
|
||||
};
|
||||
handle.dataset.largeTableResizeActive = 'true';
|
||||
table.dataset.columnResizeState = 'resizing';
|
||||
handle.setPointerCapture?.(event.pointerId);
|
||||
});
|
||||
|
||||
table.addEventListener('pointermove', (event) => {
|
||||
if (!activeResize || activeResize.table !== table) {
|
||||
return;
|
||||
}
|
||||
resizeColumn(
|
||||
table,
|
||||
activeResize.index,
|
||||
event.clientX - activeResize.startX,
|
||||
false,
|
||||
activeResize.startWidths,
|
||||
);
|
||||
});
|
||||
|
||||
table.addEventListener('pointerup', () => {
|
||||
if (!activeResize || activeResize.table !== table) {
|
||||
return;
|
||||
}
|
||||
writeStoredRatios(table, table._largeTableColumnRatios);
|
||||
activeResize.handle.dataset.largeTableResizeActive = 'false';
|
||||
table.dataset.columnResizeState = 'idle';
|
||||
activeResize = null;
|
||||
});
|
||||
|
||||
if (typeof ResizeObserver === 'function') {
|
||||
table._largeTableResizeObserver = new ResizeObserver(() => {
|
||||
applyRatios(table, table._largeTableColumnRatios, getMinWidth(table));
|
||||
});
|
||||
table._largeTableResizeObserver.observe(table.querySelector('[data-component-part="large-table-viewport"]'));
|
||||
}
|
||||
|
||||
table.addEventListener('keydown', (event) => {
|
||||
const handle = event.target.closest('[data-large-table-resize-handle]');
|
||||
if (!handle || !table.contains(handle)) {
|
||||
return;
|
||||
}
|
||||
const index = Number.parseInt(handle.dataset.largeTableResizeHandle, 10);
|
||||
const step = event.shiftKey ? KEYBOARD_LARGE_STEP : KEYBOARD_STEP;
|
||||
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
resizeColumn(table, index, event.key === 'ArrowRight' ? step : -step, true);
|
||||
} else if (event.key === 'Home' || event.key === 'End') {
|
||||
event.preventDefault();
|
||||
const minWidth = getMinWidth(table);
|
||||
const current = table._largeTableColumnWidths[index];
|
||||
const target = event.key === 'Home' ? minWidth : current + table._largeTableColumnWidths[index + 1] - minWidth;
|
||||
resizeColumn(table, index, target - current, true);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
window.sgInitLargeTableColumnResize = (options = {}) => {
|
||||
const root = options.root || document;
|
||||
getTables(root).forEach(initTable);
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => window.sgInitLargeTableColumnResize(), { once: true });
|
||||
})();
|
||||
@@ -0,0 +1,241 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const cardSelector = '[data-shared-editable-action-card]';
|
||||
const scrollRestoreStates = new WeakMap();
|
||||
|
||||
const getCard = (element) => element.closest(cardSelector);
|
||||
|
||||
const setToggleEnabled = (card, enabled) => {
|
||||
const toggle = card?.querySelector('[data-shared-editable-card-toggle]');
|
||||
if (!toggle) {
|
||||
return;
|
||||
}
|
||||
toggle.disabled = !enabled;
|
||||
toggle.setAttribute('aria-disabled', String(!enabled));
|
||||
toggle.classList.toggle('sg-button--inactive', !enabled);
|
||||
};
|
||||
|
||||
const setVisibility = (card, visible) => {
|
||||
if (!card || !card.hasAttribute('data-shared-editable-card-collapsible')) {
|
||||
return;
|
||||
}
|
||||
const readView = card.querySelector('[data-shared-editable-card-read]');
|
||||
const toggle = card.querySelector('[data-shared-editable-card-toggle]');
|
||||
card.dataset.sharedEditableCardVisible = String(visible);
|
||||
card.classList.toggle('shared-editable-action-card--collapsed', !visible);
|
||||
if (readView && card.querySelector('[data-shared-editable-card-form]')?.hidden !== false) {
|
||||
readView.hidden = !visible;
|
||||
}
|
||||
if (toggle) {
|
||||
toggle.textContent = visible ? 'Verbergen' : 'Anzeigen';
|
||||
toggle.setAttribute('aria-expanded', String(visible));
|
||||
}
|
||||
};
|
||||
|
||||
const isVisible = (card) => card?.dataset.sharedEditableCardVisible === 'true';
|
||||
|
||||
const getScrollContainer = (element) => {
|
||||
let candidate = element?.parentElement || null;
|
||||
|
||||
while (candidate && candidate !== document.body) {
|
||||
const style = window.getComputedStyle(candidate);
|
||||
const scrollable = /(auto|scroll|overlay)/.test(style.overflowY);
|
||||
if (scrollable && candidate.scrollHeight > candidate.clientHeight) {
|
||||
return candidate;
|
||||
}
|
||||
candidate = candidate.parentElement;
|
||||
}
|
||||
|
||||
return document.scrollingElement || document.documentElement;
|
||||
};
|
||||
|
||||
const captureScrollPosition = (root, card) => {
|
||||
const container = getScrollContainer(card);
|
||||
scrollRestoreStates.set(root, {
|
||||
card,
|
||||
container,
|
||||
top: container.scrollTop,
|
||||
left: container.scrollLeft,
|
||||
timer: null,
|
||||
});
|
||||
};
|
||||
|
||||
const restoreScrollPosition = (root) => {
|
||||
const state = scrollRestoreStates.get(root);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.timer) {
|
||||
window.clearTimeout(state.timer);
|
||||
}
|
||||
|
||||
state.timer = window.setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (state.container?.isConnected) {
|
||||
state.container.scrollTo({ top: state.top, left: state.left, behavior: 'auto' });
|
||||
}
|
||||
scrollRestoreStates.delete(root);
|
||||
});
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const getDirectDeleteElement = (card, selector) => Array.from(card?.children || [])
|
||||
.find((child) => child.matches(selector)) || null;
|
||||
|
||||
const setEditMode = (card, enabled) => {
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 (card.hasAttribute('data-shared-editable-card-collapsible')) {
|
||||
if (enabled) {
|
||||
setVisibility(card, true);
|
||||
} else if (!card.dataset.sharedEditableCardVisible) {
|
||||
setVisibility(card, !card.hasAttribute('data-shared-editable-card-collapsed'));
|
||||
}
|
||||
setToggleEnabled(card, !enabled);
|
||||
}
|
||||
|
||||
if (readView) {
|
||||
readView.hidden = enabled;
|
||||
}
|
||||
if (editView) {
|
||||
editView.hidden = !enabled;
|
||||
}
|
||||
editButtons.forEach((editButton) => {
|
||||
editButton.disabled = enabled;
|
||||
editButton.setAttribute('aria-disabled', String(enabled));
|
||||
editButton.classList.toggle('sg-button--inactive', enabled);
|
||||
});
|
||||
|
||||
if (card.hasAttribute('data-shared-editable-card-collapsible') && enabled && readView) {
|
||||
readView.hidden = true;
|
||||
}
|
||||
};
|
||||
|
||||
const resetDeleteConfirmation = (card) => {
|
||||
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 = '';
|
||||
}
|
||||
if (submit) {
|
||||
submit.disabled = true;
|
||||
submit.setAttribute('aria-disabled', 'true');
|
||||
submit.classList.add('sg-button--process-inactive');
|
||||
}
|
||||
if (confirmation) {
|
||||
confirmation.hidden = true;
|
||||
}
|
||||
if (stage) {
|
||||
stage.dataset.dialogOpen = 'false';
|
||||
}
|
||||
};
|
||||
|
||||
const openDeleteConfirmation = (card) => {
|
||||
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 = '';
|
||||
input.focus();
|
||||
}
|
||||
if (confirmation) {
|
||||
confirmation.hidden = false;
|
||||
}
|
||||
if (stage) {
|
||||
stage.dataset.dialogOpen = 'true';
|
||||
}
|
||||
};
|
||||
|
||||
const updateDeleteButton = (input) => {
|
||||
const card = getCard(input);
|
||||
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;
|
||||
|
||||
if (submit) {
|
||||
submit.disabled = !valid;
|
||||
submit.setAttribute('aria-disabled', String(!valid));
|
||||
submit.classList.toggle('sg-button--process-inactive', !valid);
|
||||
}
|
||||
};
|
||||
|
||||
const init = (root) => {
|
||||
if (!root || root.dataset.sharedEditableActionCardsInitialized === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
root.dataset.sharedEditableActionCardsInitialized = 'true';
|
||||
root.querySelectorAll(cardSelector).forEach((card) => setEditMode(card, false));
|
||||
root.addEventListener('click', (event) => {
|
||||
const toggle = event.target.closest('[data-shared-editable-card-toggle]');
|
||||
if (toggle && root.contains(toggle)) {
|
||||
const card = getCard(toggle);
|
||||
if (card?.querySelector('[data-shared-editable-card-form]')?.hidden === false) {
|
||||
return;
|
||||
}
|
||||
setVisibility(card, !isVisible(card));
|
||||
return;
|
||||
}
|
||||
|
||||
const edit = event.target.closest('[data-shared-editable-card-edit]');
|
||||
if (edit && root.contains(edit)) {
|
||||
const card = getCard(edit);
|
||||
captureScrollPosition(root, card);
|
||||
setEditMode(card, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const cancel = event.target.closest('[data-shared-editable-card-cancel]');
|
||||
if (cancel && root.contains(cancel)) {
|
||||
const card = getCard(cancel);
|
||||
resetDeleteConfirmation(card);
|
||||
setEditMode(card, false);
|
||||
scrollRestoreStates.delete(root);
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteButton = event.target.closest('[data-shared-editable-card-delete]');
|
||||
if (deleteButton && root.contains(deleteButton)) {
|
||||
openDeleteConfirmation(getCard(deleteButton));
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteCancel = event.target.closest('[data-shared-editable-card-delete-cancel]');
|
||||
if (deleteCancel && root.contains(deleteCancel)) {
|
||||
resetDeleteConfirmation(getCard(deleteCancel));
|
||||
}
|
||||
});
|
||||
|
||||
const scrollObserver = new MutationObserver(() => {
|
||||
const state = scrollRestoreStates.get(root);
|
||||
if (state && !root.contains(state.card)) {
|
||||
restoreScrollPosition(root);
|
||||
}
|
||||
});
|
||||
scrollObserver.observe(root, { childList: true, subtree: true });
|
||||
|
||||
root.addEventListener('input', (event) => {
|
||||
const input = event.target.closest('[data-shared-editable-card-delete-input]');
|
||||
if (input && root.contains(input)) {
|
||||
updateDeleteButton(input);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.SharedEditableActionCard = { init, resetDeleteConfirmation, setEditMode, setToggleEnabled, setVisibility, isVisible };
|
||||
}());
|
||||
@@ -0,0 +1,85 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const create = ({ contentRoot, bodyClass, hostClass, hostDataAttribute, drawerDataAttribute, backdropSelector, backMarkup, historyKey }) => {
|
||||
let host = null;
|
||||
let drawer = null;
|
||||
let trigger = null;
|
||||
let open = false;
|
||||
|
||||
const restore = () => {
|
||||
if (host && host.parentNode) {
|
||||
const mobileDrawer = host.querySelector('[' + drawerDataAttribute + ']');
|
||||
if (mobileDrawer) {
|
||||
contentRoot.appendChild(mobileDrawer);
|
||||
mobileDrawer.removeAttribute(drawerDataAttribute);
|
||||
mobileDrawer.dataset.open = 'false';
|
||||
mobileDrawer.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
host.parentNode.removeChild(host);
|
||||
}
|
||||
|
||||
if (trigger) {
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
const backdrops = backdropSelector
|
||||
? [contentRoot.querySelector(backdropSelector)].filter(Boolean)
|
||||
: Array.from(contentRoot.querySelectorAll('.sg-card-list-page-drawer-backdrop'));
|
||||
backdrops.forEach((backdrop) => {
|
||||
backdrop.dataset.open = 'false';
|
||||
backdrop.setAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
host = null;
|
||||
drawer = null;
|
||||
trigger = null;
|
||||
open = false;
|
||||
document.body.classList.remove(bodyClass);
|
||||
document.body.classList.remove('sg-card-list-page-drawer-open');
|
||||
};
|
||||
|
||||
const mount = (sourceDrawer, detailTrigger, existingHost = null) => {
|
||||
restore();
|
||||
if (!sourceDrawer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
host = existingHost || document.createElement('div');
|
||||
host.className = hostClass;
|
||||
host.setAttribute(hostDataAttribute, 'true');
|
||||
if (!existingHost) {
|
||||
host.innerHTML = backMarkup;
|
||||
}
|
||||
drawer = sourceDrawer;
|
||||
trigger = detailTrigger;
|
||||
drawer.setAttribute(drawerDataAttribute, 'true');
|
||||
drawer.dataset.open = 'true';
|
||||
drawer.setAttribute('aria-hidden', 'false');
|
||||
host.appendChild(drawer);
|
||||
document.body.appendChild(host);
|
||||
document.body.classList.add(bodyClass);
|
||||
open = true;
|
||||
|
||||
if (historyKey) {
|
||||
window.history.pushState({ ...(window.history.state || {}), [historyKey]: 'detail' }, '', window.location.href);
|
||||
}
|
||||
|
||||
return { host, drawer, trigger: detailTrigger };
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
if (window.history.state && window.history.state[historyKey] === 'detail') {
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
restore();
|
||||
};
|
||||
|
||||
return { mount, restore, close, isOpen: () => open, getHost: () => host, getDrawer: () => drawer };
|
||||
};
|
||||
|
||||
window.SharedMobileDetail = { create };
|
||||
}());
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../modules/erp/lager/mcp.php';
|
||||
|
||||
function assert_test(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
fwrite(STDERR, $message . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$tools = erp_lager_mcp_tools();
|
||||
assert_test(count($tools) === 2, 'Expected 2 ERP lager MCP tools, got ' . count($tools));
|
||||
|
||||
$names = array_map(static fn (array $tool): string => $tool['name'], $tools);
|
||||
assert_test($names === ['erp_lager.list_products', 'erp_lager.get_product'], 'Unexpected ERP lager MCP tool names');
|
||||
|
||||
$productTool = $tools[1];
|
||||
assert_test($productTool['inputSchema']['required'] === ['product_id'], 'Product detail tool must require product_id');
|
||||
assert_test($productTool['inputSchema']['properties']['product_id']['type'] === 'integer', 'Product ID must be an integer');
|
||||
assert_test(is_object($tools[0]['inputSchema']['properties']), 'List products properties must serialize as a JSON object');
|
||||
fwrite(STDOUT, "erp_lager_mcp_tools_list_test: OK\n");
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../modules/market_management/mcp.php';
|
||||
|
||||
function fail_test(string $message): void
|
||||
{
|
||||
fwrite(STDERR, $message . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function assert_test(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
fail_test($message);
|
||||
}
|
||||
}
|
||||
|
||||
function assert_schema_objects(array $schema, string $path = 'inputSchema'): void
|
||||
{
|
||||
assert_test(($schema['type'] ?? null) === 'object', $path . '.type must be object');
|
||||
assert_test(isset($schema['properties']) && (is_array($schema['properties']) || is_object($schema['properties'])), $path . '.properties must be an object-compatible map');
|
||||
|
||||
if (isset($schema['required'])) {
|
||||
assert_test(is_array($schema['required']), $path . '.required must be an array');
|
||||
foreach ($schema['required'] as $required) {
|
||||
assert_test(is_string($required), $path . '.required entries must be strings');
|
||||
}
|
||||
}
|
||||
|
||||
$properties = is_object($schema['properties'])
|
||||
? get_object_vars($schema['properties'])
|
||||
: $schema['properties'];
|
||||
foreach ($properties as $propertyName => $propertySchema) {
|
||||
assert_test(is_array($propertySchema), $path . '.properties.' . $propertyName . ' must be an object schema');
|
||||
if (($propertySchema['type'] ?? null) === 'array' && isset($propertySchema['items'])) {
|
||||
assert_test(is_array($propertySchema['items']), $path . '.properties.' . $propertyName . '.items must be an object schema');
|
||||
if (($propertySchema['items']['type'] ?? null) === 'object') {
|
||||
assert_schema_objects($propertySchema['items'], $path . '.properties.' . $propertyName . '.items');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$expected = [
|
||||
'market_management.search_markets' => 'Märkte suchen',
|
||||
'market_management.search_occurrences' => 'Marktdurchführungen suchen und auflisten',
|
||||
'market_management.get_market' => 'Markt mit Marktdurchführungen lesen',
|
||||
'market_management.create_market' => 'Markt mit verpflichtendem Veranstalter, Kategorie und optionaler Website-URL anlegen',
|
||||
'market_management.update_market' => 'Markt inklusive Veranstalter und Website-URL ändern',
|
||||
'market_management.archive_market' => 'Markt archivieren',
|
||||
'market_management.reactivate_market' => 'Markt reaktivieren',
|
||||
'market_management.list_categories' => 'Bestehende Kategorien lesen',
|
||||
'market_management.list_market_organizers' => 'Aktive Veranstalter lesen',
|
||||
'market_management.create_market_organizer' => 'Veranstalter mit optionalen Kontaktdaten anlegen',
|
||||
'market_management.list_reference_data' => 'Kategorien, Kantone und Gemeinden lesen',
|
||||
'market_management.create_municipality' => 'Lokale Gemeinde im Markt-Gemeinderegister anlegen',
|
||||
'market_management.get_overview' => 'Marktübersicht mit serverseitigen Filtern lesen',
|
||||
'market_management.create_occurrence' => 'Marktdurchführung mit mindestens einem Markttag anlegen',
|
||||
'market_management.get_occurrence' => 'Marktdurchführung mit Markt, Markttagen und berechneten Verkaufsdaten lesen',
|
||||
'market_management.get_occurrence_checklist' => 'Checkliste einer Marktdurchführung lesen',
|
||||
'market_management.update_occurrence_checklist' => 'Checkliste einer Marktdurchführung vollständig ersetzen',
|
||||
'market_management.update_occurrence' => 'Marktdurchführung ändern',
|
||||
'market_management.archive_occurrence' => 'Marktdurchführung archivieren',
|
||||
'market_management.reactivate_occurrence' => 'Marktdurchführung reaktivieren',
|
||||
'market_management.create_day' => 'Markttag anlegen',
|
||||
'market_management.update_day' => 'Markttag ändern',
|
||||
'market_management.delete_day' => 'Markttag entfernen',
|
||||
];
|
||||
|
||||
$tools = market_management_mcp_tools();
|
||||
assert_test(count($tools) === 23, 'Expected 23 MCP tools, got ' . count($tools));
|
||||
|
||||
$occurrenceTool = array_values(array_filter($tools, static fn (array $tool): bool => $tool['name'] === 'market_management.update_occurrence'))[0] ?? null;
|
||||
assert_test(is_array($occurrenceTool), 'Update occurrence MCP tool must exist');
|
||||
assert_test(isset($occurrenceTool['inputSchema']['properties']['registration_note']), 'MCP occurrence schema must expose registration_note');
|
||||
$createOccurrenceTool = array_values(array_filter($tools, static fn (array $tool): bool => $tool['name'] === 'market_management.create_occurrence'))[0] ?? null;
|
||||
assert_test(is_array($createOccurrenceTool), 'Create occurrence MCP tool must exist');
|
||||
$createProperties = $createOccurrenceTool['inputSchema']['properties'];
|
||||
assert_test(isset($createProperties['place'], $createProperties['days']['items']['properties']['date']), 'Create occurrence schema must expose both levels');
|
||||
assert_test(!isset($createProperties['days']['items']['properties']['place']), 'Occurrence fields must not be nested under days');
|
||||
$writeTools = [
|
||||
'market_management.create_market', 'market_management.update_market',
|
||||
'market_management.create_market_organizer', 'market_management.create_municipality',
|
||||
'market_management.archive_market', 'market_management.reactivate_market',
|
||||
'market_management.create_occurrence', 'market_management.update_occurrence',
|
||||
'market_management.update_occurrence_checklist', 'market_management.archive_occurrence',
|
||||
'market_management.reactivate_occurrence', 'market_management.create_day',
|
||||
'market_management.update_day', 'market_management.delete_day',
|
||||
];
|
||||
foreach ($writeTools as $writeToolName) {
|
||||
$writeTool = array_values(array_filter($tools, static fn (array $tool): bool => $tool['name'] === $writeToolName))[0] ?? null;
|
||||
assert_test(is_array($writeTool), $writeToolName . ' must exist');
|
||||
assert_test(in_array('idempotency_key', $writeTool['inputSchema']['required'] ?? [], true), $writeToolName . ' must require idempotency_key');
|
||||
assert_test(isset($writeTool['inputSchema']['properties']['idempotency_key']), $writeToolName . ' must expose idempotency_key');
|
||||
}
|
||||
|
||||
$actual = [];
|
||||
foreach ($tools as $tool) {
|
||||
assert_test(isset($tool['name'], $tool['description'], $tool['inputSchema']), 'Tool must contain name, description and inputSchema');
|
||||
assert_schema_objects($tool['inputSchema'], $tool['name'] . '.inputSchema');
|
||||
$actual[$tool['name']] = $tool['description'];
|
||||
}
|
||||
|
||||
assert_test($actual === $expected, 'MCP tool names or descriptions changed unexpectedly');
|
||||
fwrite(STDOUT, "market_management_mcp_tools_list_test: OK\n");
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../modules/market_management/service.php';
|
||||
|
||||
function fail_test(string $message): void
|
||||
{
|
||||
fwrite(STDERR, $message . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function assert_test(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
fail_test($message);
|
||||
}
|
||||
}
|
||||
|
||||
$plainText = "Test 'A' & <B> \"C\"";
|
||||
$sanitized = market_management_sanitize_multiline_html($plainText);
|
||||
assert_test($sanitized === "Test 'A' & <B> \"C\"", 'Plain text must preserve quotes and escape markup characters once');
|
||||
|
||||
$link = '<a href="https://erpnaurua.imhochrain.ch/files.php?path=Marktmanagement%2Fvertrag.pdf">Vertrag</a>';
|
||||
$sanitizedLink = market_management_sanitize_multiline_html($link);
|
||||
assert_test(str_contains($sanitizedLink, 'class="sg-hyperlink"'), 'Allowed links must remain links');
|
||||
assert_test(str_contains($sanitizedLink, 'href="https://erpnaurua.imhochrain.ch/files.php?path=Marktmanagement%2Fvertrag.pdf"'), 'Link href must remain valid');
|
||||
|
||||
fwrite(STDOUT, "market_management_sanitizer_test: OK\n");
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../modules/market_management/service.php';
|
||||
|
||||
function fail_test(string $message): void
|
||||
{
|
||||
fwrite(STDERR, $message . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function assert_test(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
fail_test($message);
|
||||
}
|
||||
}
|
||||
|
||||
$normalized = market_management_optional_url(['website_url' => 'www.stadtaffoltern.ch'], 'website_url');
|
||||
assert_test($normalized === 'https://www.stadtaffoltern.ch', 'Website without scheme must be normalized to HTTPS');
|
||||
|
||||
$existingScheme = market_management_optional_url(['website_url' => 'http://www.stadtaffoltern.ch'], 'website_url');
|
||||
assert_test($existingScheme === 'http://www.stadtaffoltern.ch', 'Existing HTTP scheme must be preserved');
|
||||
|
||||
try {
|
||||
market_management_optional_url(['website_url' => 'not a url'], 'website_url');
|
||||
fail_test('Invalid website URL must be rejected');
|
||||
} catch (InvalidArgumentException) {
|
||||
}
|
||||
|
||||
fwrite(STDOUT, "market_management_url_test: OK\n");
|
||||
Reference in new issue
Block a user