Compare commits
173
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
@@ -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;
|
||||
@@ -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,63 @@
|
||||
# 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_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,311 @@
|
||||
# Hermes: Market-Management-MCP-API
|
||||
|
||||
Stand: 2026-09-20
|
||||
Status: Aktive technische Schnittstellendokumentation
|
||||
|
||||
## 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 vollständige
|
||||
HTTPS-URL in das Feld `notes` aufnehmen. Der bestehende Text darf erhalten
|
||||
bleiben; die Position im Freitext ist frei wählbar.
|
||||
|
||||
Die URL verwendet dieses Format:
|
||||
|
||||
```text
|
||||
https://erpnaurua.imhochrain.ch/files.php?path=<RFC-3986-url-encodierter-relativer-Pfad>
|
||||
```
|
||||
|
||||
Der Parameter `path` ist relativ zum Ordner `Hermes Naurua` und enthält nicht
|
||||
den lokalen Mount-Prefix und nicht den Ordnernamen `Hermes Naurua` selbst.
|
||||
Beispiel:
|
||||
|
||||
```text
|
||||
Quelldatei: Hermes Naurua/Marktmanagement/2026 Bellevue Noël/Standmietvertrag_Naurua.pdf
|
||||
Link: https://erpnaurua.imhochrain.ch/files.php?path=Marktmanagement%2F2026%20Bellevue%20No%C3%ABl%2FStandmietvertrag_Naurua.pdf
|
||||
```
|
||||
|
||||
Leerzeichen, Umlaute und sonstige Sonderzeichen müssen vollständig URL-encodiert
|
||||
werden. Die ERP-Anzeige macht ausschließlich diese Dokument-URLs klickbar. Der
|
||||
Link ist schreibgeschützt und erfordert eine aktive ERP-Anmeldung.
|
||||
|
||||
Die Route benötigt auf dem ERP-Webserver die Konfiguration
|
||||
`HERMES_DOCUMENT_ROOT=/volume1/Hermes/Hermes Naurua`. Dieser lokale Pfad ist
|
||||
die auf DEV verifizierte Ablage der Synology-Freigabe.
|
||||
|
||||
## Fachliche Begrifflichkeit für Hermes
|
||||
|
||||
Die fachliche Hierarchie lautet:
|
||||
|
||||
```text
|
||||
Markt (market_definition)
|
||||
└── Marktdurchführung (market_occurrence)
|
||||
└── Markttag / Markttage (market_occurrence_day)
|
||||
```
|
||||
|
||||
Ein Markt ist die zentrale Marktdefinition. Eine Marktdurchführung ist eine
|
||||
konkrete Ausgabe bzw. Teilnahme an diesem Markt. Eine Marktdurchführung besitzt
|
||||
mindestens einen Markttag. OTC-Verkäufe werden immer einem konkreten Markttag
|
||||
zugeordnet; die zugehörige Marktdurchführung und der Markt ergeben sich aus
|
||||
dieser Referenz.
|
||||
|
||||
### 1. Initialisieren
|
||||
|
||||
```json
|
||||
{
|
||||
"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`
|
||||
- `market_management.list_market_organizers`
|
||||
- liefert aktive Veranstalterorganisationen
|
||||
- `market_management.create_market_organizer`
|
||||
- erforderlich: `name`
|
||||
- optional: `email`, `phone`
|
||||
- optional: `website_url` als HTTP(S)-URL des Marktes
|
||||
- `market_management.update_market`
|
||||
- erforderlich: `market_id`
|
||||
- änderbar: `name`, `category_id`, `website_url`
|
||||
- `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
|
||||
- `market_management.get_occurrence`
|
||||
- erforderlich: `occurrence_id`
|
||||
- liefert zusätzlich den Markt, Markttage, Kosten und
|
||||
berechnete Tages- und Gesamterlöse
|
||||
- `market_management.update_occurrence`
|
||||
- erforderlich: `occurrence_id`
|
||||
- änderbar: sämtliche editierbaren GUI-Marktdurchführungsfelder einschließlich
|
||||
Gemeinde, Kontaktperson, Kontakt-E-Mail und Kontakt-Telefon
|
||||
- der vom Markt geerbte Veranstalter ist nicht editierbar
|
||||
- `market_management.archive_occurrence`
|
||||
- erforderlich: `occurrence_id`
|
||||
- `market_management.reactivate_occurrence`
|
||||
- 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.
|
||||
- Markt und Marktdurchführung werden unabhängig archiviert.
|
||||
- Alle Schreibvorgänge werden serverseitig validiert.
|
||||
- Jeder schreibende MCP-Aufruf benötigt `idempotency_key`; optional kann
|
||||
`client_id` mitgegeben werden. Wiederholungen liefern dasselbe Ergebnis.
|
||||
- Umsatz- und Artikelmengen aus OTC-Verkäufen sind erst verfügbar, sobald die
|
||||
separate OTC-Schnittstelle die Zuordnung zum Markttag bereitstellt.
|
||||
|
||||
## 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,95 @@
|
||||
# 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
|
||||
- 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.
|
||||
- Hermes darf bei einem neuen Markt nur eine bestehende Kategorie referenzieren.
|
||||
- Wenn Hermes Informationen aus einer Datei verarbeitet und in einer
|
||||
Marktdurchführung speichert, muss die Quelldatei als vollständige HTTPS-URL
|
||||
im Feld `notes` referenziert werden. Die Position im bestehenden Freitext ist
|
||||
frei wählbar.
|
||||
- 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,404 @@
|
||||
# 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_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
|
||||
- Referenzdaten verwalten
|
||||
|
||||
## 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 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,48 @@
|
||||
# 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
|
||||
|
||||
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-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.
|
||||
|
||||
## 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.
|
||||
@@ -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">
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
(function initTooltipOverlayModule() {
|
||||
const getViewportWidth = () => {
|
||||
if (window.visualViewport && typeof window.visualViewport.width === 'number') {
|
||||
return window.visualViewport.width;
|
||||
}
|
||||
return window.innerWidth;
|
||||
};
|
||||
|
||||
const getPixelValue = (propertyName) => {
|
||||
const rootStyles = getComputedStyle(document.documentElement);
|
||||
const rawValue = rootStyles.getPropertyValue(propertyName).trim();
|
||||
const rootFontSize = parseFloat(rootStyles.fontSize) || 16;
|
||||
const value = parseFloat(rawValue);
|
||||
|
||||
if (Number.isNaN(value)) {
|
||||
return 0;
|
||||
}
|
||||
if (rawValue.endsWith('rem')) {
|
||||
return value * rootFontSize;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const setTooltipState = (wrap, isOpen) => {
|
||||
const trigger = wrap.querySelector('[data-component-part="tooltip-trigger"]');
|
||||
const panel = wrap.querySelector('[data-component-part="tooltip-panel"]');
|
||||
if (!trigger || !panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
wrap.dataset.tooltipOpen = String(isOpen);
|
||||
panel.hidden = !isOpen;
|
||||
trigger.setAttribute('aria-expanded', String(isOpen));
|
||||
};
|
||||
|
||||
const positionTooltip = (wrap) => {
|
||||
const trigger = wrap.querySelector('[data-component-part="tooltip-trigger"]');
|
||||
const panel = wrap.querySelector('[data-component-part="tooltip-panel"]');
|
||||
if (!trigger || !panel || panel.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportWidth = getViewportWidth();
|
||||
const safeInset = getPixelValue('--spacing-large');
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const gap = getPixelValue('--spacing-small');
|
||||
|
||||
let placement = 'bottom';
|
||||
if (triggerRect.right + (panelRect.width / 2) > viewportWidth - safeInset) {
|
||||
placement = 'left';
|
||||
} else if (triggerRect.left - (panelRect.width / 2) < safeInset) {
|
||||
placement = 'right';
|
||||
}
|
||||
|
||||
wrap.dataset.tooltipPlacement = placement;
|
||||
|
||||
if (placement === 'bottom') {
|
||||
const nextPanelRect = panel.getBoundingClientRect();
|
||||
if (nextPanelRect.bottom > window.innerHeight - safeInset
|
||||
&& triggerRect.top - panelRect.height - gap >= safeInset) {
|
||||
wrap.dataset.tooltipPlacement = 'top';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const closeTooltip = (wrap) => {
|
||||
wrap.dataset.tooltipPinned = 'false';
|
||||
wrap.dataset.tooltipHover = 'false';
|
||||
wrap.dataset.tooltipFocus = 'false';
|
||||
wrap.dataset.tooltipDismissed = 'false';
|
||||
setTooltipState(wrap, false);
|
||||
};
|
||||
|
||||
const openTooltip = (wrap) => {
|
||||
setTooltipState(wrap, true);
|
||||
positionTooltip(wrap);
|
||||
};
|
||||
|
||||
const initTooltip = (wrap) => {
|
||||
if (wrap.dataset.tooltipInit === 'true') {
|
||||
return;
|
||||
}
|
||||
wrap.dataset.tooltipInit = 'true';
|
||||
wrap.dataset.tooltipPlacement = 'bottom';
|
||||
wrap.dataset.tooltipPinned = 'false';
|
||||
wrap.dataset.tooltipHover = 'false';
|
||||
wrap.dataset.tooltipFocus = 'false';
|
||||
wrap.dataset.tooltipDismissed = 'false';
|
||||
setTooltipState(wrap, false);
|
||||
|
||||
const trigger = wrap.querySelector('[data-component-part="tooltip-trigger"]');
|
||||
if (!trigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
wrap.addEventListener('pointerenter', () => {
|
||||
if (wrap.dataset.tooltipDismissed === 'true') {
|
||||
return;
|
||||
}
|
||||
wrap.dataset.tooltipHover = 'true';
|
||||
openTooltip(wrap);
|
||||
});
|
||||
|
||||
wrap.addEventListener('pointerleave', () => {
|
||||
wrap.dataset.tooltipHover = 'false';
|
||||
wrap.dataset.tooltipDismissed = 'false';
|
||||
if (wrap.dataset.tooltipPinned !== 'true' && wrap.dataset.tooltipFocus !== 'true') {
|
||||
setTooltipState(wrap, false);
|
||||
}
|
||||
});
|
||||
|
||||
wrap.addEventListener('focusin', () => {
|
||||
if (wrap.dataset.tooltipDismissed === 'true') {
|
||||
return;
|
||||
}
|
||||
wrap.dataset.tooltipFocus = 'true';
|
||||
openTooltip(wrap);
|
||||
});
|
||||
|
||||
wrap.addEventListener('focusout', (event) => {
|
||||
if (wrap.contains(event.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
wrap.dataset.tooltipFocus = 'false';
|
||||
wrap.dataset.tooltipDismissed = 'false';
|
||||
if (wrap.dataset.tooltipPinned !== 'true' && wrap.dataset.tooltipHover !== 'true') {
|
||||
setTooltipState(wrap, false);
|
||||
}
|
||||
});
|
||||
|
||||
trigger.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const isPinned = wrap.dataset.tooltipPinned === 'true';
|
||||
wrap.dataset.tooltipPinned = String(!isPinned);
|
||||
|
||||
if (isPinned) {
|
||||
wrap.dataset.tooltipDismissed = 'true';
|
||||
setTooltipState(wrap, false);
|
||||
return;
|
||||
}
|
||||
|
||||
wrap.dataset.tooltipDismissed = 'false';
|
||||
openTooltip(wrap);
|
||||
});
|
||||
};
|
||||
|
||||
document.querySelectorAll('.sg-tooltip-wrap').forEach(initTooltip);
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
document.querySelectorAll('.sg-tooltip-wrap').forEach((wrap) => {
|
||||
if (!wrap.contains(event.target)) {
|
||||
closeTooltip(wrap);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const repositionOpenTooltips = () => {
|
||||
document.querySelectorAll('.sg-tooltip-wrap[data-tooltip-open="true"]').forEach(positionTooltip);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', repositionOpenTooltips);
|
||||
window.addEventListener('scroll', repositionOpenTooltips, true);
|
||||
})();
|
||||
@@ -224,6 +224,10 @@
|
||||
<tr><td>text-help-icon</td><td>color-font-dark</td><td>Textfarbe im Help-Icon.</td></tr>
|
||||
<tr><td>text-help-panel</td><td>color-font-dark</td><td>Textfarbe im Help-Panel.</td></tr>
|
||||
<tr><td>layout-help-panel-width</td><td>calc(interaction-height * 8)</td><td>Standardbreite des Help-Panels.</td></tr>
|
||||
<tr><td>surface-tooltip</td><td>color-light-grey</td><td>Fläche der generischen Tooltip-Sprechblase.</td></tr>
|
||||
<tr><td>text-tooltip</td><td>color-font-dark</td><td>Textfarbe der generischen Tooltip-Sprechblase.</td></tr>
|
||||
<tr><td>layout-tooltip-width</td><td>calc(interaction-height * 8)</td><td>Standardbreite der generischen Tooltip-Sprechblase.</td></tr>
|
||||
<tr><td>radius-tooltip</td><td>radius-interaction</td><td>Radius der generischen Tooltip-Sprechblase.</td></tr>
|
||||
<tr><td>icon-sandwich-line-portal</td><td>color-font-dark</td><td>Linienfarbe des Sandwich-Icons für große und kleine Variante.</td></tr>
|
||||
</tbody></table>
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
--surface-menu-panel-portal: var(--color-light-grey);
|
||||
--surface-help-icon: var(--color-medium-grey);
|
||||
--surface-help-panel: var(--color-light-grey);
|
||||
--surface-tooltip: var(--color-light-grey);
|
||||
--surface-pulldown-panel: var(--color-light-grey);
|
||||
--surface-activatable-remove: var(--surface-control-default);
|
||||
--surface-slider-track: var(--color-medium-grey);
|
||||
@@ -89,6 +90,8 @@
|
||||
--layout-mode-toggle-local-width-factor: 3;
|
||||
--layout-mode-toggle-width: calc(var(--interaction-height) * 3.5);
|
||||
--layout-help-panel-width: calc(var(--interaction-height) * 8);
|
||||
--layout-tooltip-width: calc(var(--interaction-height) * 8);
|
||||
--radius-tooltip: var(--radius-interaction);
|
||||
--text-control-default: var(--color-font-dark);
|
||||
--text-control-disabled: var(--color-dark-grey);
|
||||
--text-button-process: var(--color-font-light);
|
||||
@@ -98,6 +101,7 @@
|
||||
--text-menu-link-portal: var(--color-font-dark);
|
||||
--text-help-icon: var(--color-font-dark);
|
||||
--text-help-panel: var(--color-font-dark);
|
||||
--text-tooltip: var(--color-font-dark);
|
||||
--text-activatable-remove: var(--text-control-default);
|
||||
--text-hyperlink: var(--color-font-hyperlink);
|
||||
--icon-pulldown-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M2 2l4 4 4-4' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='square' stroke-linejoin='miter'/%3E%3C/svg%3E");
|
||||
|
||||
@@ -1239,6 +1239,95 @@
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel {
|
||||
position: absolute;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
display: block;
|
||||
width: var(--layout-tooltip-width);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: anywhere;
|
||||
padding: var(--spacing-large);
|
||||
border-radius: var(--radius-tooltip);
|
||||
color: var(--text-tooltip);
|
||||
background: var(--surface-tooltip);
|
||||
box-shadow: var(--shadow-overlay);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel {
|
||||
top: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel {
|
||||
bottom: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
right: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
left: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel::before {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel::before {
|
||||
top: calc(var(--spacing-small) * -1);
|
||||
left: 50%;
|
||||
border-width: 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent var(--surface-tooltip);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel::before {
|
||||
bottom: calc(var(--spacing-small) * -1);
|
||||
left: 50%;
|
||||
border-width: var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: var(--surface-tooltip) transparent transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel::before {
|
||||
top: 50%;
|
||||
right: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent transparent var(--surface-tooltip);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel::before {
|
||||
top: 50%;
|
||||
left: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-pulldown-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + var(--spacing-small));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,332 @@
|
||||
<?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
|
||||
{
|
||||
$occurrenceProperties = [
|
||||
'status' => ['type' => 'string'], 'place' => ['type' => ['string', 'null']], 'address' => ['type' => ['string', 'null']],
|
||||
'municipality_reference_id' => ['type' => ['integer', 'null']], 'notes' => ['type' => ['string', 'null']],
|
||||
'registration_deadline' => ['type' => ['string', 'null'], 'format' => 'date'],
|
||||
'request_or_registration_date' => ['type' => ['string', 'null'], 'format' => 'date'],
|
||||
'confirmation_date' => ['type' => ['string', 'null'], 'format' => 'date'],
|
||||
'payment_deadline' => ['type' => ['string', 'null'], 'format' => 'date'],
|
||||
'payment_status' => ['type' => ['string', 'null'], 'enum' => ['paid', 'unpaid']],
|
||||
'payment_date' => ['type' => ['string', 'null'], 'format' => 'date'], 'paid_amount' => ['type' => ['number', 'null']],
|
||||
'payment_method_code' => ['type' => ['string', 'null']], 'stand_size' => ['type' => ['string', 'null']],
|
||||
'gross_cost' => ['type' => ['number', 'null'], 'minimum' => 0], 'tax_status' => ['type' => ['string', 'null'], 'enum' => ['tax_free', 'taxable']], 'tax_rate' => ['type' => ['number', 'null'], 'minimum' => 0], 'cost_items_text' => ['type' => ['string', 'null']],
|
||||
'table_available' => ['type' => ['boolean', 'null']], 'table_rentable' => ['type' => ['boolean', 'null']],
|
||||
'own_table_required' => ['type' => ['boolean', 'null']], 'shelter_available' => ['type' => ['boolean', 'null']],
|
||||
'power_available' => ['type' => ['boolean', 'null']], 'water_available' => ['type' => ['boolean', 'null']],
|
||||
'lighting_available' => ['type' => ['boolean', 'null']], 'other_infrastructure' => ['type' => ['string', 'null']],
|
||||
'own_stand_requirements' => ['type' => ['string', 'null']], 'travel_address' => ['type' => ['string', 'null']],
|
||||
'travel_distance_km' => ['type' => ['number', 'null']], 'travel_duration_minutes' => ['type' => ['integer', 'null']],
|
||||
'parking_options' => ['type' => ['string', 'null']], 'delivery_access' => ['type' => ['string', 'null']],
|
||||
'direct_unload_possible' => ['type' => ['boolean', 'null']], 'storage_options' => ['type' => ['string', 'null']],
|
||||
'travel_notes' => ['type' => ['string', 'null']], 'cancellation_terms' => ['type' => ['string', 'null']],
|
||||
'revenue_share_terms' => ['type' => ['string', 'null']], 'other_participation_conditions' => ['type' => ['string', 'null']],
|
||||
'contract_folder_url' => ['type' => ['string', 'null'], 'format' => 'uri'],
|
||||
'contact_person_name' => ['type' => ['string', 'null']],
|
||||
'contact_email' => ['type' => ['string', 'null'], 'format' => 'email'], 'contact_phone' => ['type' => ['string', 'null']],
|
||||
];
|
||||
$dayProperties = [
|
||||
'date' => ['type' => 'string', 'format' => 'date'], 'observations' => ['type' => ['string', 'null']],
|
||||
'stand_start_time' => ['type' => ['string', 'null'], 'format' => 'time'], 'stand_end_time' => ['type' => ['string', 'null'], 'format' => 'time'],
|
||||
'setup_start_time' => ['type' => ['string', 'null'], 'format' => 'time'], 'setup_end_time' => ['type' => ['string', 'null'], 'format' => 'time'],
|
||||
'delivery_start_time' => ['type' => ['string', 'null'], 'format' => 'time'], 'delivery_end_time' => ['type' => ['string', 'null'], 'format' => 'time'],
|
||||
'inherit_from_first_day' => ['type' => ['boolean', 'null']],
|
||||
];
|
||||
$searchProperties = [
|
||||
'search' => ['type' => 'string'], 'market_definition_id' => ['type' => 'integer'], 'category_id' => ['type' => 'integer'],
|
||||
'status' => ['type' => 'string'], 'place' => ['type' => 'string'], 'address' => ['type' => 'string'],
|
||||
'municipality_reference_id' => ['type' => 'integer'], 'date_from' => ['type' => 'string', 'format' => 'date'],
|
||||
'date_to' => ['type' => 'string', 'format' => 'date'], 'include_archived' => ['type' => 'boolean'],
|
||||
'sort' => ['type' => 'string'], 'sort_direction' => ['type' => 'string', 'enum' => ['asc', 'desc']],
|
||||
'limit' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100], 'offset' => ['type' => 'integer', 'minimum' => 0],
|
||||
];
|
||||
return [
|
||||
['name' => 'market_management.search_markets', 'description' => 'Märkte suchen', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]],
|
||||
['name' => 'market_management.search_occurrences', 'description' => 'Marktdurchführungen suchen und auflisten', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]],
|
||||
['name' => 'market_management.get_market', 'description' => 'Markt mit Marktdurchführungen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]],
|
||||
['name' => 'market_management.create_market', 'description' => 'Markt mit verpflichtendem Veranstalter, Kategorie und optionaler Website-URL anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'category_id', 'organizer_id'], 'properties' => ['name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]],
|
||||
['name' => 'market_management.update_market', 'description' => 'Markt inklusive Veranstalter und Website-URL ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer'], 'name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]],
|
||||
['name' => 'market_management.archive_market', 'description' => 'Markt archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer']]]],
|
||||
['name' => 'market_management.reactivate_market', 'description' => 'Markt reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer']]]],
|
||||
['name' => 'market_management.list_categories', 'description' => 'Bestehende Kategorien lesen', 'inputSchema' => ['type' => 'object', 'properties' => []]],
|
||||
['name' => 'market_management.list_market_organizers', 'description' => 'Aktive Veranstalter lesen', 'inputSchema' => ['type' => 'object', 'properties' => []]],
|
||||
['name' => 'market_management.create_market_organizer', 'description' => 'Veranstalter mit optionalen Kontaktdaten anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name'], 'properties' => ['name' => ['type' => 'string'], 'email' => ['type' => ['string', 'null'], 'format' => 'email'], 'phone' => ['type' => ['string', 'null']]]]],
|
||||
['name' => 'market_management.list_reference_data', 'description' => 'Kategorien, Kantone und Gemeinden lesen', 'inputSchema' => ['type' => 'object', 'properties' => []]],
|
||||
['name' => 'market_management.create_municipality', 'description' => 'Lokale Gemeinde im Markt-Gemeinderegister anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name'], 'properties' => ['name' => ['type' => 'string']]]],
|
||||
['name' => 'market_management.get_overview', 'description' => 'Marktübersicht mit serverseitigen Filtern lesen', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]],
|
||||
['name' => 'market_management.create_occurrence', 'description' => 'Marktdurchführung mit mindestens einem Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id', 'days'], 'properties' => ['market_definition_id' => ['type' => 'integer'], 'status' => ['type' => 'string'], 'days' => ['type' => 'array', 'minItems' => 1, 'items' => ['type' => 'object', 'required' => ['date'], 'properties' => $dayProperties]] + $occurrenceProperties]]],
|
||||
['name' => 'market_management.get_occurrence', 'description' => 'Marktdurchführung mit Markt, Markttagen und berechneten Verkaufsdaten lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]],
|
||||
['name' => 'market_management.update_occurrence', 'description' => 'Marktdurchführung ändern', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer']] + $occurrenceProperties]],
|
||||
['name' => 'market_management.archive_occurrence', 'description' => 'Marktdurchführung archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer']]]],
|
||||
['name' => 'market_management.reactivate_occurrence', 'description' => 'Marktdurchführung reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer']]]],
|
||||
['name' => 'market_management.create_day', 'description' => 'Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'date'], 'properties' => ['occurrence_id' => ['type' => 'integer']] + $dayProperties]],
|
||||
['name' => 'market_management.update_day', 'description' => 'Markttag ändern', 'inputSchema' => ['type' => 'object', 'required' => ['day_id'], 'properties' => ['day_id' => ['type' => 'integer']] + $dayProperties]],
|
||||
['name' => 'market_management.delete_day', 'description' => 'Markttag entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['day_id'], 'properties' => ['day_id' => ['type' => 'integer']]]],
|
||||
];
|
||||
}
|
||||
|
||||
function market_management_mcp_is_write_tool(string $tool): bool
|
||||
{
|
||||
return in_array($tool, [
|
||||
'market_management.create_market', 'market_management.update_market',
|
||||
'market_management.create_market_organizer',
|
||||
'market_management.create_municipality',
|
||||
'market_management.archive_market', 'market_management.reactivate_market',
|
||||
'market_management.create_occurrence', 'market_management.update_occurrence',
|
||||
'market_management.archive_occurrence', 'market_management.reactivate_occurrence',
|
||||
'market_management.create_day', 'market_management.update_day', 'market_management.delete_day',
|
||||
], 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.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),
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+497
-64
@@ -50,6 +50,180 @@ 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_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-markets-large-table="true" data-markets-search="' . auth_escape_html($search) . '" data-markets-sort-column="' . auth_escape_html($sortColumn) . '" data-markets-sort-direction="' . auth_escape_html($sortDirection) . '" data-markets-limit="' . $limit . '" data-markets-page-size="' . $pageSize . '" role="table" aria-label="Marktdurchführungen">';
|
||||
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header">';
|
||||
$html[] = '<div class="sg-strong">Marktdurchführungen</div>';
|
||||
$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 . '"><button class="sg-large-table__sort-button" type="button" aria-label="' . auth_escape_html($ariaLabel) . '" data-markets-sort-button="true" data-markets-sort-column="' . auth_escape_html($column) . '" data-markets-sort-direction="' . auth_escape_html($targetDirection) . '"><span class="sg-large-table__sort-label">' . auth_escape_html($label) . '</span><span class="sg-large-table__sort-icon" aria-hidden="true" data-direction="' . auth_escape_html($iconDirection) . '"></span></button></div>';
|
||||
$headerIndex++;
|
||||
}
|
||||
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader">Verkaufte Produkte</div>';
|
||||
$html[] = '</div>';
|
||||
|
||||
foreach ($rows as $index => $row) {
|
||||
$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"><div class="sg-navigation-card-layout sg-large-table__load-more-layout"><div class="sg-navigation-card-block"><article class="sg-card" data-component="card" data-pattern="navigation-card" aria-label="Mehr laden"><div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-pattern-part="navigation-card-segment"><div class="sg-navigation-card-center"><a class="sg-hyperlink" href="#" data-component="hyperlink" data-markets-load-more-trigger="true">Mehr laden</a></div></div></article></div></div></div></div>';
|
||||
}
|
||||
$html[] = '</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'] ?? [];
|
||||
$html = [];
|
||||
$html[] = '<div data-market-definitions-content="true" data-market-definitions-csrf-token="' . auth_escape_html(auth_csrf_token()) . '"><section class="sg-card-list-page" aria-label="Märkte">';
|
||||
$html[] = '<article class="sg-card" data-component="basic-card"><div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-component-part="card-body"><div class="sg-component-row sg-basic-card__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" aria-haspopup="dialog" aria-expanded="false" data-component="button" data-market-definition-create>Neuer Markt</button></div></div></article>';
|
||||
$html[] = '<article class="sg-card sg-large-table sg-large-table--markets" data-component="large-table" data-market-definitions-table="true" role="table" aria-label="Märkte">';
|
||||
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header"><div class="sg-strong">Märkte</div></div>';
|
||||
$html[] = '<div class="sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row sg-large-table__row--header" role="row" data-component-part="large-table-header-row">';
|
||||
foreach (['Markt', 'Kategorie', 'Veranstalter', 'Website', 'Durchführungen', 'Status'] as $label) {
|
||||
$html[] = '<div class="sg-large-table__cell sg-large-table__cell--header" role="columnheader">' . auth_escape_html($label) . '</div>';
|
||||
}
|
||||
$html[] = '</div>';
|
||||
foreach ($rows as $index => $row) {
|
||||
$rowClass = 'sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row';
|
||||
if ($index % 2 === 1) {
|
||||
$rowClass .= ' sg-large-table__row--striped-light';
|
||||
}
|
||||
$website = (string) ($row['website_url'] ?? '');
|
||||
$websiteValue = $website === '' ? 'Nicht hinterlegt' : $website;
|
||||
$status = ($row['archived_at'] ?? null) === null ? 'Aktiv' : 'Archiviert';
|
||||
$marketId = (int) ($row['id'] ?? 0);
|
||||
$marketName = (string) ($row['name'] ?? '');
|
||||
$organizerName = (string) ($row['organizer_name'] ?? '');
|
||||
$html[] = '<div class="' . $rowClass . '" role="row" data-component-part="large-table-row" data-market-definition-row data-market-definition-id="' . $marketId . '">';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell"><a class="sg-hyperlink" href="#" aria-haspopup="dialog" aria-expanded="false" data-market-definition-open data-market-definition-id="' . $marketId . '" data-market-definition-name="' . auth_escape_html($marketName) . '" data-market-definition-category-id="' . (int) ($row['category_id'] ?? 0) . '" data-market-definition-category-name="' . auth_escape_html((string) ($row['category_name'] ?? $row['category_code'] ?? '')) . '" data-market-definition-organizer-id="' . (int) ($row['organizer_id'] ?? 0) . '" data-market-definition-organizer-name="' . auth_escape_html($organizerName) . '" data-market-definition-website="' . auth_escape_html($website) . '">' . auth_escape_html($marketName) . '</a></div>';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html((string) ($row['category_name'] ?? $row['category_code'] ?? '')) . '</div>';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html($organizerName) . '</div>';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html($websiteValue) . '</div>';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html((string) ($row['occurrence_count'] ?? 0)) . '</div>';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html($status) . '</div>';
|
||||
$html[] = '</div>';
|
||||
}
|
||||
$html[] = '</article></section>';
|
||||
$html[] = '<div class="sg-card-list-page-drawer-backdrop" aria-hidden="true" data-market-definition-drawer-backdrop="true" data-open="false"></div>';
|
||||
$html[] = '<aside class="sg-card-list-page-drawer" aria-label="Markt-Details" aria-hidden="true" data-market-definition-drawer="true" data-open="false"><div class="sg-card-list-page-drawer__content" data-market-definition-drawer-content><article class="sg-card sg-card--content-card" data-component="content-card" data-shared-editable-action-card data-market-definition-card><header class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen sg-object-card__header"><h2 class="sg-strong sg-text-on-dark" data-market-definition-read-title>Markt</h2><div class="sg-component-row"><button class="sg-interaction-element sg-button sg-button--active" type="button" data-market-definition-edit data-shared-editable-card-edit>Bearbeiten</button></div></header><div class="sg-card-segment sg-card-segment--body sg-card-segment--white" data-market-definition-read-card data-shared-editable-card-read><dl class="market-detail-grid"><div class="market-detail-row"><dt>Marktname</dt><dd data-market-definition-read-name></dd></div><div class="market-detail-row"><dt>Kategorie/Marktart</dt><dd data-market-definition-read-category></dd></div><div class="market-detail-row"><dt>Veranstalter</dt><dd data-market-definition-read-organizer></dd></div><div class="market-detail-row"><dt>Website</dt><dd data-market-definition-read-website></dd></div></dl></div><div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-market-definition-create-card data-shared-editable-card-form hidden><h2 class="sg-strong" data-market-definition-form-title>Markt bearbeiten</h2><div class="sg-form-sections-card-wrapper" data-pattern="form-sections"><form class="sg-form-sections-card" data-market-definition-form><div class="sg-form-sections-card__body" data-pattern-part="form-body"><div class="sg-form-sections-card__section"><div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Marktname<span class="market-required-field-indicator" aria-hidden="true">*</span></span><input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" name="name" required data-market-definition-name></label></div><div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Kategorie/Marktart<span class="market-required-field-indicator" aria-hidden="true">*</span></span><select class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable sg-pulldown" name="category_id" required data-market-definition-category><option value="">Bitte auswählen</option>';
|
||||
foreach ($categories as $category) {
|
||||
$html[] = '<option value="' . (int) ($category['id'] ?? 0) . '">' . auth_escape_html((string) ($category['name'] ?? $category['code'] ?? '')) . '</option>';
|
||||
}
|
||||
$html[] = '</select></label></div><div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Veranstalter<span class="market-required-field-indicator" aria-hidden="true">*</span></span><select class="sg-interaction-element sg-input-single-line sg-pulldown" name="organizer_id" required data-market-definition-organizer><option value="">Bitte auswählen</option>';
|
||||
foreach ($organizers as $organizer) {
|
||||
$html[] = '<option value="' . (int) ($organizer['id'] ?? 0) . '">' . auth_escape_html((string) ($organizer['name'] ?? '')) . '</option>';
|
||||
}
|
||||
$html[] = '<option value="__new__">Neuen Veranstalter anlegen</option></select></label></div><div class="sg-form-sections-card__field-group" data-market-definition-new-organizer hidden><label class="sg-labeled-input-row"><span class="sg-label">Name des neuen Veranstalters<span class="market-required-field-indicator" aria-hidden="true">*</span></span><input class="sg-interaction-element sg-input-single-line" type="text" name="organizer_name" data-market-definition-organizer-name></label><label class="sg-labeled-input-row"><span class="sg-label">E-Mail</span><input class="sg-interaction-element sg-input-single-line" type="email" name="organizer_email" data-market-definition-organizer-email></label><label class="sg-labeled-input-row"><span class="sg-label">Telefon</span><input class="sg-interaction-element sg-input-single-line" type="tel" name="organizer_phone" data-market-definition-organizer-phone></label></div><div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Website</span><input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="url" name="website_url" placeholder="https://" data-market-definition-website></label></div></div></div><footer class="sg-form-sections-card__actions-segment" data-pattern-part="form-actions-segment"><div class="sg-form-sections-card__actions" data-pattern-part="form-actions"><button class="sg-interaction-element sg-button sg-button--active sg-form-sections-card__action" type="button" data-market-definition-cancel data-shared-editable-card-cancel>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process sg-form-sections-card__action" type="submit">Speichern</button></div></footer><p class="sg-body" data-market-definition-status aria-live="polite"></p></form></div></div></div></article></div></aside></div>';
|
||||
return implode('', $html);
|
||||
}
|
||||
|
||||
function auth_render_bestellungen_large_table(array $bestellungenTable): string
|
||||
{
|
||||
$rows = $bestellungenTable['rows'] ?? [];
|
||||
@@ -282,7 +456,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'
|
||||
@@ -328,17 +505,17 @@ function auth_render_lager_historical_lots(array $lagerProducts): string
|
||||
$emptyDate = $qtyNet <= 0 ? auth_format_lager_date($lot['last_out_at'] ?? null) : 'Nicht eindeutig';
|
||||
$html[] = '<article class="sg-card sg-card--content-card" data-component="content-card" aria-label="Vergangene Charge">'
|
||||
. '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen" data-component-part="card-header"><div class="sg-strong sg-text-on-dark">Vergangene Charge</div></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body">'
|
||||
. '<p class="sg-body"><strong>Naurua-Chargennummer:</strong> ' . auth_escape_html((string) ($lot['lot_number'] ?? 'Nicht hinterlegt')) . '<br>'
|
||||
. '<strong>Status:</strong> Abgeschlossen<br>'
|
||||
. '<strong>Eingangsmenge:</strong> ' . auth_format_lager_quantity((float) ($lot['qty_in'] ?? 0)) . '<br>'
|
||||
. '<strong>Verbrauchte Menge:</strong> ' . auth_format_lager_quantity((float) ($lot['qty_out'] ?? 0)) . '<br>'
|
||||
. '<strong>Verbrauchszeitraum:</strong> ' . auth_format_lager_date($lot['first_in_at'] ?? null) . ' – ' . auth_format_lager_date($lot['last_out_at'] ?? null) . '<br>'
|
||||
. '<strong>Aufgebraucht am:</strong> ' . $emptyDate . '<br>'
|
||||
. '<strong>MHD:</strong> ' . auth_format_lager_date($lot['expiry_date'] ?? null) . '<br>'
|
||||
. '<strong>Hersteller:</strong> ' . auth_escape_html((string) ($lot['manufacturer_name'] ?? 'Nicht hinterlegt')) . '<br>'
|
||||
. '<strong>Hersteller-Chargennummer:</strong> ' . auth_escape_html((string) ($lot['manufacturer_lot_number'] ?? 'Nicht hinterlegt')) . '</p>'
|
||||
. '</div></article>';
|
||||
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body"><dl class="market-detail-grid">'
|
||||
. '<div class="market-detail-row"><dt>Naurua-Chargennummer</dt><dd>' . auth_escape_html((string) ($lot['lot_number'] ?? 'Nicht hinterlegt')) . '</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>Status</dt><dd>Abgeschlossen</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>Eingangsmenge</dt><dd>' . auth_format_lager_quantity((float) ($lot['qty_in'] ?? 0)) . '</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>Verbrauchte Menge</dt><dd>' . auth_format_lager_quantity((float) ($lot['qty_out'] ?? 0)) . '</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>Verbrauchszeitraum</dt><dd>' . auth_format_lager_date($lot['first_in_at'] ?? null) . ' – ' . auth_format_lager_date($lot['last_out_at'] ?? null) . '</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>Aufgebraucht am</dt><dd>' . $emptyDate . '</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>MHD</dt><dd>' . auth_format_lager_date($lot['expiry_date'] ?? null) . '</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>Hersteller</dt><dd>' . auth_escape_html((string) ($lot['manufacturer_name'] ?? 'Nicht hinterlegt')) . '</dd></div>'
|
||||
. '<div class="market-detail-row"><dt>Hersteller-Chargennummer</dt><dd>' . auth_escape_html((string) ($lot['manufacturer_lot_number'] ?? 'Nicht hinterlegt')) . '</dd></div>'
|
||||
. '</dl></div></article>';
|
||||
}
|
||||
$html[] = '</div></div>';
|
||||
}
|
||||
@@ -364,18 +541,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 +554,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 +624,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 +641,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 +654,7 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
function render_auth_home_page(array $user, array $otcProducts = [], array $bestellungenTable = [], array $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 +674,26 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
$otcProductRows .= '</div>';
|
||||
}
|
||||
|
||||
$otcMarketOccurrenceOptions = [];
|
||||
$otcMarketDayOptions = '';
|
||||
foreach ($otcMarketOptions as $option) {
|
||||
$occurrenceId = (int) ($option['occurrence_id'] ?? 0);
|
||||
$marketDefinitionId = (int) ($option['market_definition_id'] ?? 0);
|
||||
$dayId = (int) ($option['day_id'] ?? 0);
|
||||
if ($occurrenceId <= 0 || $marketDefinitionId <= 0 || $dayId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$marketName = (string) ($option['market_name'] ?? '');
|
||||
$otcMarketOccurrenceOptions[$marketDefinitionId] = $marketName;
|
||||
$day = (string) ($option['date'] ?? '');
|
||||
$dayLabel = $day === '' ? '' : (new DateTimeImmutable($day))->format('d.m.Y');
|
||||
$otcMarketDayOptions .= '<option value="' . $dayId . '" data-market-definition-id="' . $marketDefinitionId . '" data-market-occurrence-id="' . $occurrenceId . '">' . auth_escape_html($dayLabel) . '</option>';
|
||||
}
|
||||
$otcMarketOccurrenceOptionHtml = '<option value="">Markt auswählen</option>';
|
||||
foreach ($otcMarketOccurrenceOptions as $marketDefinitionId => $marketName) {
|
||||
$otcMarketOccurrenceOptionHtml .= '<option value="' . $marketDefinitionId . '">' . auth_escape_html($marketName) . '</option>';
|
||||
}
|
||||
|
||||
if ($otcProductRows === '') {
|
||||
$otcProductRows = '<p class="sg-body sg-form-sections-card__sentence">Keine aktiven OTC-Produkte erfasst.</p>';
|
||||
}
|
||||
@@ -512,16 +702,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 +726,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',
|
||||
],
|
||||
@@ -568,6 +764,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>';
|
||||
@@ -676,6 +873,32 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" id="otc-city" value="Oberengstringen" data-otc-order-city>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '</section>';
|
||||
echo '<section class="sg-form-sections-card__chapter" aria-labelledby="otc-market-link-title">';
|
||||
echo '<h2 id="otc-market-link-title" class="sg-strong sg-form-sections-card__chapter-title">Marktbezug</h2>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<label class="sg-checkbox-field-option sg-checkbox-field-option--inactive-selectable sg-body" data-component="checkbox-field" data-component-state="inactive-selectable">';
|
||||
echo '<button class="sg-checkbox-field sg-checkbox-field--inactive-selectable" type="button" role="checkbox" aria-checked="false" aria-label="Verkauf ist auf einem Markttag" data-otc-market-link-toggle>';
|
||||
echo '<span class="sg-checkbox-field__mark" aria-hidden="true">✓</span>';
|
||||
echo '</button><span>Verkauf ist auf einem Markttag</span>';
|
||||
echo '<input type="hidden" name="market_sale" value="0" data-otc-market-link-enabled>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<label class="sg-labeled-input-row"><span class="sg-label">Markt</span>';
|
||||
echo '<select class="sg-interaction-element sg-input-single-line sg-input-single-line--disabled sg-form-disabled sg-pulldown sg-pulldown--disabled" disabled aria-disabled="true" data-otc-market-occurrence>';
|
||||
echo $otcMarketOccurrenceOptionHtml;
|
||||
echo '</select>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<label class="sg-labeled-input-row"><span class="sg-label">Markttag</span>';
|
||||
echo '<select class="sg-interaction-element sg-input-single-line sg-input-single-line--disabled sg-form-disabled sg-pulldown sg-pulldown--disabled" disabled aria-disabled="true" data-otc-market-day>';
|
||||
echo '<option value="">Zuerst Markt auswählen</option>' . $otcMarketDayOptions;
|
||||
echo '</select>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '</section>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<div class="sg-body sg-otc-order-form__status hidden" data-otc-order-success>Bestellung erfolgreich erfasst! Die Bestellnummer wird automatisch generiert.</div>';
|
||||
echo '<div class="sg-body sg-otc-order-form__status sg-otc-order-form__status--error hidden" data-otc-order-error></div>';
|
||||
@@ -697,6 +920,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo '<div class="sg-delete-confirmation-pattern__list" data-otc-confirmation-products aria-label="Produkte"></div>';
|
||||
echo '<p class="sg-body sg-delete-confirmation-pattern__text"><strong>Gesamtzahl Flaschen: <span data-otc-confirmation-quantity>0</span></strong></p>';
|
||||
echo '<p class="sg-body sg-delete-confirmation-pattern__text"><strong>Gesamtbetrag: CHF <span data-otc-confirmation-total>0.00</span></strong></p>';
|
||||
echo '<p class="sg-body sg-otc-order-form__status sg-otc-order-form__status--error hidden" data-otc-confirmation-error aria-live="polite"></p>';
|
||||
echo '<div class="sg-delete-confirmation-pattern__actions">';
|
||||
echo '<button class="sg-interaction-element sg-button sg-button--active" type="button" data-otc-confirmation-cancel>Abbrechen</button>';
|
||||
echo '<button class="sg-interaction-element sg-button sg-button--process" type="button" data-otc-confirmation-submit>Verkaufen</button>';
|
||||
@@ -706,6 +930,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo '</div>';
|
||||
echo '</section>';
|
||||
echo '<script src="/scripts/help-icon-overlays.js"></script>';
|
||||
echo '<script src="/scripts/shared-editable-action-card.js?v=20260920-2"></script>';
|
||||
echo '<script>';
|
||||
echo 'const portalModuleNavigation = ' . $moduleNavigation . ';';
|
||||
echo 'const portalModuleContentCards = ' . $moduleContentCards . ';';
|
||||
@@ -714,18 +939,23 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const heading = document.querySelector('[data-main-heading]');";
|
||||
echo " if (heading && portalModuleHeadings[moduleName]) { heading.textContent = portalModuleHeadings[moduleName]; }";
|
||||
echo "};";
|
||||
echo "const renderLeftNavigation = (moduleName) => {";
|
||||
echo "const updatePortalLocation = (moduleName, entryName = '') => { const url = new URL(window.location.href); url.searchParams.set('portal_module', moduleName); if (entryName) { url.searchParams.set('portal_section', entryName); } else { url.searchParams.delete('portal_section'); } window.history.replaceState({ ...(window.history.state || {}), portalModule: moduleName, portalSection: entryName }, '', url); };";
|
||||
echo "const renderLeftNavigation = (moduleName, requestedEntry = '') => {";
|
||||
echo " const title = document.querySelector('[data-left-navigation-title]');";
|
||||
echo " const contentTitle = document.querySelector('[data-left-navigation-content-title]');";
|
||||
echo " const contentSection = document.querySelector('.sg-left-navigation-pattern__group-card--content');";
|
||||
echo " const contentBody = document.querySelector('[data-left-navigation-content-body]');";
|
||||
echo " const menu = document.getElementById('left-navigation-menu');";
|
||||
echo " if (title) { title.textContent = moduleName; }";
|
||||
echo " if (!menu) { return; }";
|
||||
echo " const entries = portalModuleNavigation[moduleName] || [];";
|
||||
echo " const activeEntry = entries[0] || '';";
|
||||
echo " const activeEntry = entries.includes(requestedEntry) ? requestedEntry : (entries[0] || '');";
|
||||
echo " if (contentTitle) { contentTitle.textContent = activeEntry; }";
|
||||
echo " if (contentSection) { contentSection.setAttribute('aria-hidden', 'false'); }";
|
||||
echo " if (contentBody) { contentBody.innerHTML = portalModuleContentCards[activeEntry] || ''; }";
|
||||
echo " if (activeEntry === 'Bestellungen') { initBestellungenBindings(); }";
|
||||
echo " if (activeEntry === '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 +964,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " button.type = 'button';";
|
||||
echo " button.setAttribute('role', 'tab');";
|
||||
echo " button.setAttribute('data-component-part', 'tab-button');";
|
||||
echo " button.setAttribute('aria-selected', String(index === 0));";
|
||||
echo " button.setAttribute('aria-selected', String(entry === activeEntry));";
|
||||
echo " button.textContent = entry;";
|
||||
echo " menu.appendChild(button);";
|
||||
echo " });";
|
||||
@@ -743,11 +973,15 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " menu.querySelectorAll('.sg-tab-button').forEach((otherButton) => {";
|
||||
echo " otherButton.setAttribute('aria-selected', String(otherButton === button));";
|
||||
echo " });";
|
||||
echo " const entryName = button.textContent.trim();";
|
||||
echo " if (contentTitle) { contentTitle.textContent = entryName; }";
|
||||
echo " if (contentBody) { contentBody.innerHTML = portalModuleContentCards[entryName] || ''; }";
|
||||
echo " if (entryName === 'Bestellungen') { initBestellungenBindings(); }";
|
||||
echo " if (entryName === '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 +993,10 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " otherButton.setAttribute('aria-selected', String(isActive));";
|
||||
echo " otherButton.dataset.componentState = isActive ? 'active' : 'inactive';";
|
||||
echo " });";
|
||||
echo " renderMainHeading(button.textContent.trim());";
|
||||
echo " renderLeftNavigation(button.textContent.trim());";
|
||||
echo " const moduleName = button.textContent.trim();";
|
||||
echo " updatePortalLocation(moduleName);";
|
||||
echo " renderMainHeading(moduleName);";
|
||||
echo " renderLeftNavigation(moduleName);";
|
||||
echo " });";
|
||||
echo " });";
|
||||
echo "});";
|
||||
@@ -831,10 +1067,11 @@ 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 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');";
|
||||
@@ -850,6 +1087,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 +1103,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 +1138,183 @@ 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 linked = escaped.replace(/https:\\/\\/erpnaurua\\.imhochrain\\.ch\\/files\\.php\\?path=[^\\s<]+/g, (url) => '<a class=\"sg-hyperlink\" href=\"' + url + '\" target=\"_blank\" rel=\"noopener\">' + url + '</a>'); return linked.replace(/\\r?\\n/g, '<br>'); };";
|
||||
echo " const marketBoolean = (value) => value === true || value === 'true' || value === 1 || value === '1' ? 'Ja' : value === false || value === 'false' || value === 0 || value === '0' ? 'Nein' : 'Nicht hinterlegt';";
|
||||
echo " const marketMoney = (value) => value === null || value === undefined || value === '' ? 'Nicht hinterlegt' : 'CHF ' + Number(value).toFixed(2);";
|
||||
echo " const marketContactName = (contact) => { if (!contact) { return 'Nicht hinterlegt'; } return marketDisplay([contact.first_name, contact.last_name].filter(Boolean).join(' ')); };";
|
||||
echo " const marketDate = (value) => { if (!value) { return 'Nicht hinterlegt'; } const parts = String(value).split('-'); return parts.length === 3 ? parts[2] + '.' + parts[1] + '.' + parts[0] : marketEscape(value); };";
|
||||
echo " const marketStatusLabel = (value) => ({ open: 'Offen', waitlist: 'Warteliste', registered: 'Angemeldet', confirmation_pending: 'Bestätigung ausstehend', confirmed: 'Bestätigt', cancelled: 'Abgesagt', completed: 'Abgeschlossen' }[value] || value || 'Nicht hinterlegt');";
|
||||
echo " const marketStatusSegment = (value) => ({ waitlist: 'sg-card-segment--signal-red', registered: 'sg-card-segment--signal-yellow', confirmation_pending: 'sg-card-segment--signal-yellow', confirmed: 'sg-card-segment--signal-green' }[value] || 'sg-card-segment--gray');";
|
||||
echo " const marketRow = (label, value) => '<div class=\"market-detail-row\"><dt>' + marketEscape(label) + '</dt><dd>' + marketDisplay(value) + '</dd></div>';";
|
||||
echo " const initializeMunicipalityFields = (scope) => { scope.querySelectorAll('select[name=\"municipality_reference_id\"]').forEach((select) => { if (!select.querySelector('option[value=\"__new__\"]')) { const option = document.createElement('option'); option.value = '__new__'; option.textContent = 'Neue Gemeinde'; select.append(option); } const form = select.closest('form'); if (!form) { return; } let fields = form.querySelector('[data-market-new-municipality]'); if (!fields) { fields = document.createElement('div'); fields.className = 'sg-form-sections-card__field-group'; fields.dataset.marketNewMunicipality = ''; fields.hidden = true; fields.innerHTML = '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Gemeinde</span><input class=\"sg-interaction-element sg-input-single-line\" type=\"text\" name=\"municipality_name\" data-market-new-municipality-name></label>'; select.closest('.sg-form-sections-card__field-group')?.after(fields); } const sync = () => { const visible = select.value === '__new__'; fields.hidden = !visible; const input = fields.querySelector('[data-market-new-municipality-name]'); if (input) { input.disabled = !visible; input.setAttribute('aria-disabled', String(!visible)); } }; if (select.dataset.municipalityBound !== 'true') { select.addEventListener('change', sync); select.dataset.municipalityBound = 'true'; } sync(); }); };";
|
||||
echo " const syncMunicipalityReadValue = (scope) => { const drawer = scope.querySelector('[data-market-drawer]'); if (!drawer?.dataset.marketPayload) { return; } let payload = {}; try { payload = JSON.parse(drawer.dataset.marketPayload); } catch (error) { return; } const value = payload.occurrence?.municipality_name || 'Nicht hinterlegt'; scope.querySelectorAll('.market-detail-row').forEach((row) => { if (row.querySelector('dt')?.textContent?.trim() !== 'Gemeinde') { return; } const output = row.querySelector('dd'); if (output && output.textContent !== value) { output.textContent = value; } }); };";
|
||||
echo " const marketCard = (title, body, headerClass = 'sg-card-segment--darkgreen', actions = '', scope = '', id = '', values = null) => { const header = '<header class=\"sg-card-segment sg-card-segment--header ' + headerClass + ' sg-object-card__header\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">' + title + '</div>' + (actions ? '<div class=\"sg-component-row\">' + actions + '</div>' : '') + '</header>'; return '<article class=\"sg-card sg-card--content-card market-detail-card\" data-component=\"content-card\"' + (scope ? ' data-shared-editable-action-card' : '') + '>' + header + '<div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\"' + (scope ? ' data-shared-editable-card-read' : '') + '>' + body + '</div>' + (scope ? '<div class=\"sg-card-segment sg-card-segment--body sg-card-segment--gray\" data-shared-editable-card-form hidden>' + marketEditForm(scope, id, values) + '</div>' : '') + '</article>'; };";
|
||||
echo " const marketStatusCard = (id, values) => { const headerClass = marketStatusSegment(values.status); const textClass = headerClass === 'sg-card-segment--gray' ? '' : 'sg-text-on-dark'; const actions = marketEditButton('status', id); const header = '<header class=\"sg-card-segment sg-card-segment--header ' + headerClass + ' sg-object-card__header\" data-component-part=\"card-header\"><div class=\"sg-strong ' + textClass + '\">Status: ' + marketEscape(marketStatusLabel(values.status)) + '</div><div class=\"sg-component-row\">' + actions + '</div></header>'; return '<article class=\"sg-card sg-card--content-card market-detail-card\" data-component=\"content-card\" data-shared-editable-action-card>' + header + '<div class=\"sg-card-segment sg-card-segment--body sg-card-segment--gray\" data-shared-editable-card-form hidden>' + marketEditForm('status', id, values) + '</div></article>'; };";
|
||||
echo " const marketEditButton = () => '<button class=\"sg-interaction-element sg-button sg-button--active\" type=\"button\" data-shared-editable-card-edit>Bearbeiten</button>';";
|
||||
echo " const marketHumanizeError = (error) => { const message = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'; const labels = { table_available: 'Tisch vorhanden', table_rentable: 'Tisch mietbar', own_table_required: 'Eigener Tisch erforderlich', shelter_available: 'Überdachung', power_available: 'Strom', water_available: 'Wasser', lighting_available: 'Beleuchtung', direct_unload_possible: 'Direktes Entladen' }; const match = message.match(/^([a-z_]+) muss boolean sein\\.$/); return match && labels[match[1]] ? labels[match[1]] + ' muss Ja oder Nein sein.' : message; };";
|
||||
echo " document.addEventListener('submit', (event) => { const form = event.target.closest('[data-market-edit-form]'); if (!form) { return; } ['table_available', 'table_rentable', 'own_table_required', 'shelter_available', 'power_available', 'water_available', 'lighting_available', 'direct_unload_possible'].forEach((field) => { const control = form.elements.namedItem(field); if (!control) { return; } const label = control.selectedOptions?.[0]?.textContent?.trim(); control.value = label === 'Ja' ? '1' : label === 'Nein' ? '0' : control.value === '' || label === 'Nicht hinterlegt' ? '' : control.value; }); }, true);";
|
||||
echo " const marketEditStatusObserver = new MutationObserver(() => { contentRoot.querySelectorAll('[data-market-edit-status], [data-market-create-status]').forEach((status) => { const message = status.textContent.trim(); if (!message || message === 'Speichern läuft …') { return; } const humanMessage = marketHumanizeError(new Error(message)); if (humanMessage !== message) { status.textContent = humanMessage; } status.classList.add('sg-form-validation-text'); status.setAttribute('role', 'alert'); }); }); marketEditStatusObserver.observe(contentRoot, { childList: true, characterData: true, subtree: true });";
|
||||
echo " const marketEditField = (key, label, type, value, options = [], required = false, minDate = '') => { const current = value === null || value === undefined ? '' : String(value); const requiredAttributes = required ? ' required aria-required=\"true\"' : ''; const minAttribute = type === 'date' && minDate ? ' min=\"' + marketEscape(minDate) + '\"' : ''; const disabledAttributes = ' disabled aria-disabled=\"true\"'; const control = type === 'readonly' ? '<input class=\"sg-interaction-element sg-input-single-line sg-input-single-line--disabled sg-form-disabled\" type=\"text\" name=\"' + key + '\" value=\"' + marketEscape(current) + '\"' + disabledAttributes + '>' : type === 'readonly-textarea' ? '<textarea class=\"sg-input-multi-line sg-form-disabled\" name=\"' + key + '\" rows=\"4\"' + disabledAttributes + '>' + marketEscape(current) + '</textarea>' : type === 'textarea' ? '<textarea class=\"sg-interaction-element sg-input-multi-line\" name=\"' + key + '\" rows=\"4\"' + requiredAttributes + '>' + marketEscape(current) + '</textarea>' : type === 'select' ? '<select class=\"sg-interaction-element sg-input-single-line\" name=\"' + key + '\"' + requiredAttributes + '>' + options.map(([optionValue, optionLabel, contactEmail = '', contactPhone = '']) => '<option value=\"' + marketEscape(optionValue) + '\" data-contact-email=\"' + marketEscape(contactEmail) + '\" data-contact-phone=\"' + marketEscape(contactPhone) + '\"' + (String(optionValue) === current ? ' selected' : '') + '>' + marketEscape(optionLabel) + '</option>').join('') + '</select>' : '<input class=\"sg-interaction-element sg-input-single-line\" type=\"' + type + '\" name=\"' + key + '\" value=\"' + marketEscape(current) + '\"' + requiredAttributes + minAttribute + '>'; const requiredIndicator = required ? '<span class=\"market-required-field-indicator\" aria-hidden=\"true\">*</span>' : ''; return '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">' + marketEscape(label) + requiredIndicator + '</span>' + control + '</label>'; };";
|
||||
echo " const marketDeletePattern = (id, body) => '<div class=\"sg-delete-confirmation-pattern__stage sg-delete-confirmation-pattern__host\" data-pattern=\"overlay-card\" data-dialog-open=\"false\"><div class=\"sg-delete-confirmation-pattern__target\" data-shared-editable-action-card data-shared-editable-delete-token=\"DELETE\" data-market-delete-occurrence-id=\"' + marketEscape(id) + '\"><div class=\"sg-component-row\"><button class=\"sg-interaction-element sg-button sg-button--warning\" type=\"button\" data-component=\"button\" data-component-variant=\"warning\" data-shared-editable-card-delete data-market-delete-button>Löschen</button></div>' + body + '<article class=\"sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card\" aria-label=\"Löschbestätigung\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"market-delete-title-' + marketEscape(id) + '\" data-overlay-dialog=\"delete\" data-shared-editable-card-delete-confirmation hidden><div class=\"sg-card-segment sg-card-segment--body sg-delete-confirmation-pattern__body\"><p class=\"sg-body sg-delete-confirmation-pattern__text\" id=\"market-delete-title-' + marketEscape(id) + '\"><strong>Möchtest du diese Marktdurchführung wirklich löschen?</strong></p><p class=\"sg-body sg-delete-confirmation-pattern__text\">Du kannst das nicht rückgängig machen. Bestätige durch Eingabe von <span class=\"sg-delete-confirmation-pattern__code\">DELETE</span>.</p><label class=\"sg-labeled-input-row sg-delete-confirmation-pattern__input-row\"><span class=\"sg-label\">Bestätigung</span><input class=\"sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable\" type=\"text\" placeholder=\"DELETE\" aria-label=\"Löschbestätigung durch DELETE\" data-shared-editable-card-delete-input></label><p class=\"sg-body\" data-market-delete-status aria-live=\"polite\"></p><div class=\"sg-delete-confirmation-pattern__actions\"><button class=\"sg-interaction-element sg-button sg-button--active\" type=\"button\" data-shared-editable-card-delete-cancel>Abbrechen</button><button class=\"sg-interaction-element sg-button sg-button--process sg-button--process-inactive\" type=\"button\" disabled aria-disabled=\"true\" data-shared-editable-card-delete-submit data-market-delete-submit>Löschen</button></div></div></article></div></div>' ;";
|
||||
echo " const marketDeletePatternWithTitle = (id, title, body) => { const template = document.createElement('template'); template.innerHTML = marketDeletePattern(id, body); const target = template.content.querySelector('[data-market-delete-occurrence-id]'); const row = target?.querySelector(':scope > .sg-component-row'); if (target && row) { const card = document.createElement('article'); card.className = 'sg-card sg-card--content-card market-delete-confirmation-card'; const header = document.createElement('header'); header.className = 'sg-card-segment sg-card-segment--header sg-card-segment--gray sg-object-card__header'; const heading = document.createElement('h2'); heading.className = 'sg-heading-h2 market-delete-confirmation-card__title'; heading.textContent = title; header.append(heading, row); card.append(header); target.prepend(card); } return template.innerHTML; };";
|
||||
echo " const marketEditForm = (scope, id, values) => { const boolOptions = [['', 'Nicht hinterlegt'], ['1', 'Ja'], ['0', 'Nein']]; const statusOptions = [['open', 'Offen'], ['waitlist', 'Warteliste'], ['registered', 'Angemeldet'], ['confirmation_pending', 'Bestätigung ausstehend'], ['confirmed', 'Bestätigt'], ['cancelled', 'Abgesagt'], ['completed', 'Abgeschlossen']]; const referenceOptions = values.reference_options || {}; const categoryOptions = [['', 'Nicht hinterlegt'], ...(Array.isArray(referenceOptions.categories) ? referenceOptions.categories.map((option) => [option.id, option.name]) : [])]; const municipalityOptions = [['', 'Nicht hinterlegt'], ...(Array.isArray(referenceOptions.municipalities) ? referenceOptions.municipalities.map((option) => [option.id, option.name]) : [])]; const partyOptions = [['', 'Nicht hinterlegt'], ...(Array.isArray(referenceOptions.parties) ? referenceOptions.parties.map((option) => [option.id, option.name]) : [])]; const contactOptions = [['', 'Nicht hinterlegt'], ...(Array.isArray(referenceOptions.contacts) ? referenceOptions.contacts.map((option) => [option.id, [[option.first_name, option.last_name].filter(Boolean).join(' ') || 'Nicht hinterlegt', option.party_name].filter(Boolean).join(' – '), option.email || '', option.phone || '']) : [])]; const costSummary = values.cost_summary || {}; const formValues = Object.assign({}, values, { gross_cost: marketMoney(costSummary.gross_amount), net_cost: marketMoney(costSummary.net_amount), tax_cost: marketMoney(costSummary.tax_amount), open_cost: marketMoney(costSummary.open_amount), cost_items_text: values.cost_items_text, gross_revenue_display: marketMoney(values.gross_revenue), sold_item_count_display: marketDisplay(values.sold_item_count) }); const fieldSets = { status: [['status', 'Status', 'select', statusOptions]], overview: [['market_name', 'Markt', 'readonly'], ['category_name', 'Kategorie/Marktart', 'readonly'], ['website_url', 'Website', 'readonly'], ['place', 'Ort', 'text'], ['address', 'Adresse', 'textarea'], ['municipality_reference_id', 'Gemeinde', 'select', municipalityOptions], ['organization_contact_id', 'Veranstalter', 'select', partyOptions], ['contact_person_id', 'Kontaktperson', 'select', contactOptions], ['contact_email', 'Kontakt E-Mail', 'email'], ['contact_phone', 'Kontakt Telefon', 'tel']], participation: [['request_or_registration_date', 'Anmeldung / Anfrage', 'date'], ['registration_deadline', 'Anmeldefrist', 'date'], ['confirmation_date', 'Bestätigung', 'date'], ['payment_deadline', 'Zahlungsfrist', 'date'], ['payment_status', 'Zahlungsstatus', 'select', [['', 'Nicht hinterlegt'], ['paid', 'Bezahlt'], ['unpaid', 'Unbezahlt']]], ['payment_date', 'Zahlungsdatum', 'date'], ['paid_amount', 'Bezahlter Betrag', 'number'], ['payment_method_code', 'Zahlungsart', 'text'], ['gross_cost', 'Gesamtkosten brutto', 'readonly'], ['net_cost', 'Gesamtkosten netto', 'readonly'], ['tax_cost', 'Steuern', 'readonly'], ['open_cost', 'Offener Betrag', 'readonly'], ['cost_items_text', 'Kostenpositionen', 'readonly-textarea']], infrastructure: [['stand_size', 'Standgrösse', 'text'], ['table_available', 'Tisch vorhanden', 'select', boolOptions], ['table_rentable', 'Tisch mietbar', 'select', boolOptions], ['own_table_required', 'Eigener Tisch erforderlich', 'select', boolOptions], ['shelter_available', 'Überdachung', 'select', boolOptions], ['power_available', 'Strom', 'select', boolOptions], ['water_available', 'Wasser', 'select', boolOptions], ['lighting_available', 'Beleuchtung', 'select', boolOptions], ['other_infrastructure', 'Weitere Infrastruktur', 'textarea'], ['own_stand_requirements', 'Anforderungen an den eigenen Stand', 'textarea']], travel: [['travel_address', 'Reiseadresse', 'textarea'], ['travel_distance_km', 'Distanz (km)', 'number'], ['travel_duration_minutes', 'Reisedauer (Minuten)', 'number'], ['parking_options', 'Parkmöglichkeiten', 'textarea'], ['delivery_access', 'Anlieferung', 'textarea'], ['direct_unload_possible', 'Direktes Entladen', 'select', boolOptions], ['storage_options', 'Lagerung', 'textarea'], ['travel_notes', 'Besondere Anreisehinweise', 'textarea']], notes: [['cancellation_terms', 'Rücktrittsbedingungen', 'textarea'], ['revenue_share_terms', 'Umsatzbeteiligung', 'textarea'], ['other_participation_conditions', 'Teilnahmebedingungen', 'textarea'], ['notes', 'Notizen', 'textarea'], ['contract_folder_url', 'Vertragsordner', 'url']], day: [['date', 'Datum', 'date'], ['observations', 'Beobachtungen', 'textarea'], ['stand_start_time', 'Standzeit von', 'time'], ['stand_end_time', 'Standzeit bis', 'time'], ['setup_start_time', 'Aufbau von', 'time'], ['setup_end_time', 'Aufbau bis', 'time'], ['delivery_start_time', 'Anlieferung von', 'time'], ['delivery_end_time', 'Anlieferung bis', 'time'], ['gross_revenue_display', 'Umsatz', 'readonly'], ['sold_item_count_display', 'Verkaufte Artikel', 'readonly']] }; const requiredFields = { status: new Set(['status']), overview: new Set(), day: new
Line truncated
|
||||
echo " const marketDeletePatternWithTitleAndDays = (id, title, body) => { const html = marketDeletePatternWithTitle(id, title, body); const template = document.createElement('template'); template.innerHTML = html; const target = template.content.querySelector('[data-market-delete-occurrence-id]'); const dayGroup = target?.querySelector('.market-days-group'); if (dayGroup) { const heading = document.createElement('h2'); heading.className = 'sg-heading-h2 market-days-group__title'; heading.textContent = 'Markttage'; dayGroup.before(heading); } return template.innerHTML; };";
|
||||
echo " const marketCreateForm = (marketDefinitions) => { const options = [['', 'Bitte auswählen'], ...(Array.isArray(marketDefinitions) ? marketDefinitions.map((market) => [market.id, market.name + (market.category_name ? ' – ' + market.category_name : '')]) : [])]; const fields = '<div class=\"sg-form-sections-card__field-group\">' + marketEditField('market_definition_id', 'Markt', 'select', '', options, true) + '</div><div class=\"sg-form-sections-card__field-group\">' + marketEditField('date', 'Erster Markttag', 'date', '', [], true) + '</div>'; return '<article class=\"sg-card sg-card--content-card market-detail-card\" data-component=\"content-card\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen sg-object-card__header\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Pflichtangaben für neue Marktdurchführung</div></header><div class=\"sg-card-segment sg-card-segment--body sg-card-segment--gray\"><div class=\"sg-form-sections-card-wrapper\" data-pattern=\"form-sections\"><form class=\"sg-form-sections-card market-create-form\" data-market-create-form=\"true\"><div class=\"sg-form-sections-card__body\" data-pattern-part=\"form-body\"><div class=\"sg-form-sections-card__section\">' + fields + '</div></div><footer class=\"sg-form-sections-card__actions-segment\" data-pattern-part=\"form-actions-segment\"><div class=\"sg-form-sections-card__actions\" data-pattern-part=\"form-actions\"><button class=\"sg-interaction-element sg-button sg-button--active sg-form-sections-card__action\" type=\"button\" data-market-create-cancel>Abbrechen</button><button class=\"sg-interaction-element sg-button sg-button--process sg-form-sections-card__action\" type=\"submit\">Speichern</button></div></footer><p class=\"sg-body\" data-market-create-status aria-live=\"polite\"></p></form></div></div></article>'; };";
|
||||
echo " const buildUrl = (params) => { const url = new URL(window.location.href); Object.entries(params).forEach(([key, value]) => { if (value === null || value === undefined || value === '') { url.searchParams.delete(key); } else { url.searchParams.set(key, String(value)); } }); return url; };";
|
||||
echo " const replaceFragment = (html) => { const doc = new DOMParser().parseFromString(html, 'text/html'); const fragment = doc.querySelector('[data-markets-content]'); if (!fragment) { return false; } contentRoot.innerHTML = fragment.outerHTML; return Boolean(getTable()); };";
|
||||
echo " const loadFragment = async (params) => { if (searchTimer) { window.clearTimeout(searchTimer); searchTimer = null; } const input = getSearchInput(); const wasFocused = input ? document.activeElement === input : false; const response = await fetch(buildUrl({ ...params, market_fragment: '1' }).toString(), { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { return; } const replaced = replaceFragment(await response.text()); if (!replaced) { return; } window.history.replaceState({}, '', buildUrl(params)); bindTable(); if (wasFocused) { const nextInput = getSearchInput(); nextInput?.focus({ preventScroll: true }); if (nextInput) { nextInput.setSelectionRange(nextInput.value.length, nextInput.value.length); } } };";
|
||||
echo " const syncSearchState = () => { const input = getSearchInput(); const wrap = input?.closest('[data-component=\'single-line-input\']'); if (!input || !wrap) { return; } const hasValue = input.value.trim() !== ''; wrap.setAttribute('data-has-value', String(hasValue)); wrap.dataset.componentState = hasValue ? 'active' : 'inactive-selectable'; };";
|
||||
echo " const bindTable = () => { const table = getTable(); if (!table) { return; } const input = getSearchInput(); const clearButton = table.querySelector('[data-markets-search-clear]'); if (input && input.dataset.marketsBound !== 'true') { input.dataset.marketsBound = 'true'; input.addEventListener('input', () => { syncSearchState(); if (searchTimer) { window.clearTimeout(searchTimer); } searchTimer = window.setTimeout(() => { void loadFragment({ market_search: getSearchValue(), market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getPageSize() }); }, 500); }); } if (clearButton && clearButton.dataset.marketsBound !== 'true') { clearButton.dataset.marketsBound = 'true'; clearButton.addEventListener('click', () => { const currentInput = getSearchInput(); if (currentInput) { currentInput.value = ''; } syncSearchState(); void loadFragment({ market_search: '', market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getPageSize() }); }); } syncSearchState(); };";
|
||||
echo " let marketMobileDetailHost = null; let marketOpeningMobileDetail = false; let marketDetailOpen = false;";
|
||||
echo " const getMarketDrawer = () => document.querySelector('[data-market-mobile-drawer]') || contentRoot.querySelector('[data-market-drawer]');";
|
||||
echo " const getMarketBackdrop = () => contentRoot.querySelector('[data-market-drawer-backdrop]');";
|
||||
echo " const setMarketDrawerState = (open) => { const drawer = getMarketDrawer(); const backdrop = getMarketBackdrop(); if (!drawer || !backdrop) { return; } drawer.dataset.open = String(open); drawer.setAttribute('aria-hidden', String(!open)); backdrop.dataset.open = String(open); backdrop.setAttribute('aria-hidden', String(!open)); document.body.classList.toggle('sg-card-list-page-drawer-open', open); };";
|
||||
echo " const applyMarketTabs = (root) => { const group = root.querySelector('[data-pattern=\"card-gruppe-mit-tastennavigation\"] [role=\"tablist\"]'); if (group) { group.dataset.component = 'tab-navigation'; group.dataset.componentSize = 'large'; group.querySelectorAll('[data-market-day-tab]').forEach((tab) => { tab.dataset.componentPart = 'tab-button'; tab.classList.remove('sg-button--active'); }); } root.querySelectorAll('[data-market-day-tab]').forEach((tab) => { if (tab.dataset.marketTabBound === 'true') { return; } tab.dataset.marketTabBound = 'true'; tab.addEventListener('click', () => { const tabGroup = tab.closest('[data-pattern=\"card-gruppe-mit-tastennavigation\"]'); tabGroup?.querySelectorAll('[data-market-day-tab]').forEach((candidate) => candidate.setAttribute('aria-selected', String(candidate === tab))); tabGroup?.querySelectorAll('[role=\"tabpanel\"]').forEach((panel) => { panel.hidden = panel.id !== tab.getAttribute('aria-controls'); }); tab.focus(); }); }); };";
|
||||
echo " const syncMarketDayAddButton = (root = contentRoot) => { if (!root.querySelector('[data-market-inherit-row]')) { enhanceMarketDayInheritance(root); } const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; const days = Array.isArray(payload?.occurrence?.days) ? payload.occurrence.days.filter((day) => !day.__draft).slice().sort((left, right) => String(left.date).localeCompare(String(right.date))) : []; const nextDate = (date) => { const value = new Date(String(date) + 'T00:00:00Z'); value.setUTCDate(value.getUTCDate() + 1); return value.toISOString().slice(0, 10); }; root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=\"day\"]').forEach((form) => { const input = form.querySelector('[name=\"date\"]'); if (!input) { return; } const index = days.findIndex((day) => String(day.id) === String(form.dataset.marketEditId)); const minimum = form.dataset.marketEditDraft === 'true' ? days.at(-1)?.date : index > 0 ? days[index - 1].date : ''; input.min = minimum ? nextDate(minimum) : ''; }); const addButton = root.querySelector('[data-market-day-add]'); if (!addButton) { return; } const editing = Array.from(root.querySelectorAll('[data-market-edit-form]')).some((form) => form.closest('[data-shared-editable-card-form]')?.hidden === false); addButton.disabled = editing; addButton.setAttribute('aria-disabled', String(editing)); addButton.classList.toggle('sg-button--active', !editing); addButton.classList.toggle('sg-button--inactive', editing); };";
|
||||
echo " const enhanceMarketDayInheritance = (root) => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; const days = Array.isArray(payload?.occurrence?.days) ? payload.occurrence.days.filter((day) => !day.__draft).slice().sort((left, right) => String(left.date).localeCompare(String(right.date))) : []; const firstDayId = days[0]?.id; root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=\"day\"]').forEach((form) => { const dayId = form.dataset.marketEditId; if (!dayId || String(dayId) === String(firstDayId)) { return; } const dateGroup = form.querySelector('[name=\"date\"]')?.closest('.sg-form-sections-card__field-group'); const observationsGroup = form.querySelector('[name=\"observations\"]')?.closest('.sg-form-sections-card__field-group'); if (!dateGroup || !observationsGroup) { return; } let row = form.querySelector('[data-market-inherit-row]'); if (!row) { row = document.createElement('div'); row.className = 'sg-form-sections-card__field-group'; row.dataset.marketInheritRow = 'true'; row.innerHTML = '<label class=\"sg-checkbox-field-option sg-body\" data-component=\"checkbox-field\" data-component-state=\"default\"><button class=\"sg-checkbox-field\" type=\"button\" role=\"checkbox\" aria-checked=\"false\" aria-label=\"Ab hier alle Werte aus Tag 1 übernehmen\" data-market-inherit-toggle><span class=\"sg-checkbox-field__mark\" aria-hidden=\"true\">✓</span></button><span>Ab hier alle Werte aus Tag 1 übernehmen</span><input type=\"hidden\" name=\"inherit_from_first_day\" value=\"0\"></label>'; observationsGroup.after(row); row.querySelector('[data-market-inherit-toggle]').addEventListener('click', (event) => { event.stopImmediatePropagation(); const toggle = row.querySelector('[data-market-inherit-toggle]'); const checked = toggle.getAttribute('aria-checked') !== 'true'; toggle.setAttribute('aria-checked', String(checked)); toggle.classList.toggle('sg-form-active', checked); row.querySelector('[name=\"inherit_from_first_day\"]').value = checked ? '1' : '0'; form.querySelectorAll('input, select, textarea').forEach((control) => { if (control.name !== 'date' && control.name !== 'inherit_from_first_day' && control.name !== 'observations') { control.disabled = checked; control.setAttribute('aria-disabled', String(checked)); control.classList.toggle('sg-form-disabled', checked); control.classList.toggle('sg-input-single-line--disabled', checked && control.tagName === 'INPUT'); } }); syncMarketDayAddButton(root); }); } const day = days.find((candidate) => String(candidate.id) === String(dayId)); const inherited = Boolean(day?.inherit_from_first_day); const toggle = row.querySelector('[data-market-inherit-toggle]'); toggle.setAttribute('aria-checked', String(inherited)); toggle.classList.toggle('sg-form-active', inherited); row.querySelector('[name=\"inherit_from_first_day\"]').value = inherited ? '1' : '0'; form.querySelectorAll('input, select, textarea').forEach((control) => { if (control.name !== 'date' && control.name !== 'inherit_from_first_day' && control.name !== 'observations') { control.disabled = inherited; control.setAttribute('aria-disabled', String(inherited)); control.classList.toggle('sg-form-disabled', inherited); control.classList.toggle('sg-input-single-line--disabled', inherited && control.tagName === 'INPUT'); } }); }); };";
|
||||
echo " const marketDayDeleteConfirmation = (id, label) => '<article class=\"sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card\" aria-label=\"Löschbestätigung für Markttag\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"market-day-delete-title-' + marketEscape(id) + '\" data-overlay-dialog=\"delete\" data-market-day-delete-confirmation data-shared-editable-card-delete-confirmation hidden><div class=\"sg-card-segment sg-card-segment--body sg-delete-confirmation-pattern__body\"><p class=\"sg-body sg-delete-confirmation-pattern__text\" id=\"market-day-delete-title-' + marketEscape(id) + '\"><strong>Möchtest du den Markttag ' + marketEscape(label) + ' wirklich löschen?</strong></p><p class=\"sg-body sg-delete-confirmation-pattern__text\">Du kannst das nicht rückgängig machen. Bestätige durch Eingabe von <span class=\"sg-delete-confirmation-pattern__code\">DELETE</span>.</p><label class=\"sg-labeled-input-row sg-delete-confirmation-pattern__input-row\"><span class=\"sg-label\">Bestätigung</span><input class=\"sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable\" type=\"text\" placeholder=\"DELETE\" aria-label=\"Löschbestätigung durch DELETE\" data-shared-editable-card-delete-input></label><p class=\"sg-body\" data-market-day-delete-status aria-live=\"polite\"></p><div class=\"sg-delete-confirmation-pattern__actions\"><button class=\"sg-interaction-element sg-button sg-button--active\" type=\"button\" data-shared-editable-card-delete-cancel>Abbrechen</button><button class=\"sg-interaction-element sg-button sg-button--process sg-button--process-inactive\" type=\"button\" disabled aria-disabled=\"true\" data-shared-editable-card-delete-submit data-market-day-delete-submit>Löschen</button></div></div></article>';";
|
||||
echo " const enhanceMarketDayDeleteControls = (root) => { const cards = Array.from(root.querySelectorAll('.market-days-group .sg-content-cards-group > [data-shared-editable-action-card]')); cards.forEach((card, index) => { if (index === 0 || card.querySelector('[data-market-edit-form][data-market-edit-draft=\"true\"]')) { return; } const form = card.querySelector('[data-market-edit-form][data-market-edit-scope=\"day\"]'); const actions = card.querySelector('.sg-object-card__header .sg-component-row'); if (!form || !actions || actions.querySelector('[data-market-day-delete-button]')) { return; } const dayId = form.dataset.marketEditId || ''; const label = card.querySelector('.sg-object-card__header .sg-strong')?.textContent?.trim() || 'diesen Markttag'; if (!dayId) { return; } const stage = document.createElement('div'); stage.className = 'sg-delete-confirmation-pattern__stage sg-delete-confirmation-pattern__host'; stage.dataset.pattern = 'overlay-card'; stage.dataset.dialogOpen = 'false'; card.classList.add('sg-delete-confirmation-pattern__target'); card.parentNode.replaceChild(stage, card); stage.appendChild(card); card.dataset.marketDayDeleteId = dayId; card.dataset.sharedEditableDeleteToken = 'DELETE'; actions.insertAdjacentHTML('afterbegin', '<button class=\"sg-interaction-element sg-button sg-button--warning\" type=\"button\" data-component=\"button\" data-component-variant=\"warning\" data-shared-editable-card-delete data-market-day-delete-button>Löschen</button>'); card.insertAdjacentHTML('beforeend', marketDayDeleteConfirmation(dayId, label)); }); };";
|
||||
echo " const rerenderMarketDetail = (payload) => { const drawer = getMarketDrawer(); const content = drawer?.querySelector('[data-market-drawer-content]'); if (!drawer || !content) { return; } drawer.dataset.marketPayload = JSON.stringify(payload); content.innerHTML = renderMarketDetailWithActions(payload); applyMarketTabs(content); enhanceMarketDayDeleteControls(content); syncMarketDayAddButton(content); };";
|
||||
echo " const addMarketDayDraft = () => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; if (!payload?.occurrence) { return; } const days = Array.isArray(payload.occurrence.days) ? payload.occurrence.days : []; if (days.some((day) => day.__draft)) { return; } days.push({ id: 'draft-' + Date.now(), date: '', __draft: true }); payload.occurrence.days = days; rerenderMarketDetail(payload); const draftEdit = contentRoot.querySelector('[data-market-edit-form][data-market-edit-draft=\"true\"]')?.closest('[data-shared-editable-action-card]')?.querySelector('[data-shared-editable-card-edit]'); draftEdit?.click(); window.setTimeout(() => syncMarketDayAddButton(contentRoot), 0); };";
|
||||
echo " const discardMarketDayDraft = () => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; if (!payload?.occurrence || !Array.isArray(payload.occurrence.days)) { return; } const days = payload.occurrence.days.filter((day) => !day.__draft); if (days.length === payload.occurrence.days.length) { return; } payload.occurrence.days = days; rerenderMarketDetail(payload); };";
|
||||
echo " const closeMarketDrawer = () => { setMarketDrawerState(false); contentRoot.querySelectorAll('[data-market-drawer-open]').forEach((button) => button.setAttribute('aria-expanded', 'false')); };";
|
||||
echo " const renderMarketDetail = (payload) => { const market = payload.market || {}; const occurrence = payload.occurrence || {}; const days = Array.isArray(occurrence.days) ? occurrence.days : []; const hasDraftDay = days.some((day) => day.__draft); const organizer = occurrence.organizer || {}; const organization = organizer.organization || {}; const contactPerson = organizer.contact_person || {}; const website = market.website_url ? '<a class=\"sg-hyperlink\" href=\"' + marketEscape(market.website_url) + '\" target=\"_blank\" rel=\"noopener\">' + marketEscape(market.website_url) + '</a>' : 'Nicht hinterlegt'; const contract = occurrence.contract_folder_url ? '<a class=\"sg-hyperlink\" href=\"' + marketEscape(occurrence.contract_folder_url) + '\" target=\"_blank\" rel=\"noopener\">Vertragsordner öffnen</a>' : 'Nicht hinterlegt'; const overview = marketRow('Markt', market.name) + marketRow('Kategorie', market.category_name || market.category_code) + '<div class=\"market-detail-row\"><dt>Website</dt><dd>' + website + '</dd></div>' + marketRow('Ort', occurrence.place) + marketRow('Adresse', occurrence.address) + marketRow('Gemeinde', occurrence.municipality_reference_id) + marketRow('Veranstalter', organization.name) + marketRow('Kontaktperson', marketContactName(contactPerson)) + marketRow('Kontakt E-Mail', contactPerson.email) + marketRow('Kontakt Telefon', contactPerson.phone); const dates = days.map((day) => marketDate(day.date)).filter((date) => date !== 'Nicht hinterlegt').join(' – '); const costSummary = occurrence.cost_summary || {}; const participation = marketRow('Anmeldung / Anfrage', occurrence.request_or_registration_date) + marketRow('Anmeldefrist', occurrence.registration_deadline) + marketRow('Bestätigung', occurrence.confirmation_date) + marketRow('Zahlungsfrist', occurrence.payment_deadline) + marketRow('Zahlungsstatus', occurrence.payment_status) + marketRow('Zahlungsdatum', occurrence.payment_date) + marketRow('Bezahlter Betrag', marketMoney(occurrence.paid_amount)) + marketRow('Zahlungsart', occurrence.payment_method_code) + marketRow('Gesamtkosten brutto', marketMoney(costSummary.gross_amount)) + marketRow('Gesamtkosten netto', marketMoney(costSummary.net_amount)) + marketRow('Steuern', marketMoney(costSummary.tax_amount)) + marketRow('Offener Betrag', marketMoney(costSummary.open_amount)) + '<div class=\"market-detail-row\"><dt>Kostenpositionen</dt><dd>' + marketDisplay(occurrence.cost_items_text) + '</dd></div>'; const infrastructure = marketRow('Standgrösse', occurrence.stand_size) + marketRow('Tisch vorhanden', marketBoolean(occurrence.table_available)) + marketRow('Tisch mietbar', marketBoolean(occurrence.table_rentable)) + marketRow('Eigener Tisch erforderlich', marketBoolean(occurrence.own_table_required)) + marketRow('Überdachung', marketBoolean(occurrence.shelter_available)) + marketRow('Strom', marketBoolean(occurrence.power_available)) + marketRow('Wasser', marketBoolean(occurrence.water_available)) + marketRow('Beleuchtung', marketBoolean(occurrence.lighting_available)) + marketRow('Weitere Infrastruktur', occurrence.other_infrastructure) + marketRow('Anforderungen an den eigenen Stand', occurrence.own_stand_requirements); const travel = marketRow('Reiseadresse', occurrence.travel_address) + marketRow('Distanz (km)', occurrence.travel_distance_km) + marketRow('Reisedauer (Minuten)', occurrence.travel_duration_minutes) + marketRow('Parkmöglichkeiten', occurrence.parking_options) + marketRow('Anlieferung', occurrence.delivery_access) + marketRow('Direktes Entladen', marketBoolean(occurrence.direct_unload_possible)) + marketRow('Lagerung', occurrence.storage_options) + marketRow('Besondere Anreisehinweise', occurrence.travel_notes); const notes = marketRow('Teilnahmebedingungen', occurrence.other_participation_conditions) + marketRow('Rücktrittsbedingungen', occurrence.cancellation_terms) + marketRow('Umsatzbeteiligung', occurrence.revenue_share_terms) + marketRow('Notizen', occurrence.notes) + '<div class=\"market-detail-row\"><dt>Vertragsordner</dt><dd>' + contract + '</dd></div>'; const salesSummary = occurrence.sales_summary || {}; const dayTabs = days.map((day, index) => { const label = day.__draft ? 'Neuer Tag' : marketDate(day.date); const selected = day.__draft || (!hasDraftDay && index === 0); return '<button class=\"sg-interaction-element sg-button sg-button--active sg-tab-button\" type=\"button\" role=\"tab\" aria-selected=\"' + (selected ? 'true' : 'false') + '\" aria-controls=\"market-day-panel-' + day.id + '\" id=\"market-day-tab-' + day.id + '\" data-market-day-tab=\"true\">' + label + '</button>'; }).join(''); const addDayTab = '<button class=\"sg-interaction-element sg-button sg-button--active sg-tab-button market-day-add-button\" type=\"button\" data-market-day-add=\"true\" aria-label=\"Markttag hinzufügen\" title=\"Markttag hinzufügen\">+</button>'; const dayPanels = days.map((day, index) => { const label = day.__draft ? 'Neuer Tag' : marketDa
Line truncated
|
||||
echo " const renderMarketDetailWithActions = (payload) => marketDeletePatternWithTitleAndDays(payload.occurrence?.id || '', marketDrawerTitle(payload), renderMarketDetail(payload));";
|
||||
echo " const marketDrawerTitle = (payload) => { const marketName = payload.market?.name || 'Marktdurchführung'; const dates = (Array.isArray(payload.occurrence?.days) ? payload.occurrence.days : []).map((day) => marketDate(day.date)).filter(Boolean); return dates.length ? marketName + ', Marktdurchführung ' + dates.join(', ') : marketName; };";
|
||||
echo " const openMarketDrawer = async (trigger) => { const drawer = getMarketDrawer(); if (!drawer) { return; } const title = drawer.querySelector('[data-market-drawer-title]'); const content = drawer.querySelector('[data-market-drawer-content]'); setMarketDrawerState(true); if (title) { title.textContent = 'Marktdurchführung'; } if (content) { content.innerHTML = '<p class=\"sg-body\">Marktdetails werden geladen …</p>'; } try { const response = await fetch('/api/market-occurrence.php?id=' + encodeURIComponent(trigger.dataset.marketOccurrenceId || ''), { credentials: 'same-origin' }); const payload = await response.json(); if (!response.ok || !payload.ok) { throw new Error(payload.error || 'Marktdetails konnten nicht geladen werden.'); } drawer.dataset.marketPayload = JSON.stringify(payload); if (title) { title.textContent = marketDrawerTitle(payload); } if (content) { content.innerHTML = renderMarketDetailWithActions(payload); applyMarketTabs(content); syncMarketDayAddButton(content); } } catch (error) { if (content) { content.innerHTML = '<p class=\"sg-body\">' + marketEscape(error.message) + '</p>'; } } contentRoot.querySelectorAll('[data-market-drawer-open]').forEach((button) => button.setAttribute('aria-expanded', String(button === trigger))); };";
|
||||
echo " const openNewMarketDrawer = async () => { const drawer = getMarketDrawer(); if (!drawer) { return; } const title = drawer.querySelector('[data-market-drawer-title]'); const content = drawer.querySelector('[data-market-drawer-content]'); setMarketDrawerState(true); if (title) { title.textContent = 'Neue Marktdurchführung'; } if (content) { content.innerHTML = '<p class=\"sg-body\">Pflichtangaben werden geladen …</p>'; } try { const response = await fetch('/api/market-occurrence.php?action=create_options', { credentials: 'same-origin' }); const payload = await response.json(); if (!response.ok || !payload.ok) { throw new Error(payload.error || 'Pflichtangaben konnten nicht geladen werden.'); } if (content) { content.innerHTML = marketCreateForm(payload.market_definitions || []); } } catch (error) { if (content) { content.innerHTML = '<p class=\"sg-body\">' + marketEscape(error.message) + '</p>'; } } };";
|
||||
echo " const restoreMarketList = () => { if (marketMobileDetailHost?.parentNode) { const drawer = marketMobileDetailHost.querySelector('[data-market-mobile-drawer]'); if (drawer) { contentRoot.appendChild(drawer); drawer.removeAttribute('data-market-mobile-drawer'); drawer.dataset.open = 'false'; drawer.setAttribute('aria-hidden', 'true'); } marketMobileDetailHost.parentNode.removeChild(marketMobileDetailHost); } marketMobileDetailHost = null; marketDetailOpen = false; document.body.classList.remove('sg-market-mobile-detail-open'); bindTable(); };";
|
||||
echo " const openMarketMobileDetail = (trigger) => { restoreMarketList(); const sourceDrawer = contentRoot.querySelector('[data-market-drawer]'); if (!sourceDrawer) { return; } const host = document.createElement('div'); host.className = 'sg-market-mobile-detail-host'; host.dataset.marketMobileDetailHost = 'true'; host.innerHTML = '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\"><div class=\"sg-card-segment sg-card-segment--body\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-market-detail-back>zurück</button></div></div></article></div></div>'; sourceDrawer.dataset.marketMobileDrawer = 'true'; sourceDrawer.dataset.open = 'true'; sourceDrawer.setAttribute('aria-hidden', 'false'); host.appendChild(sourceDrawer); document.body.appendChild(host); marketMobileDetailHost = host; document.body.classList.add('sg-market-mobile-detail-open'); marketOpeningMobileDetail = true; void openMarketDrawer(trigger).finally(() => { marketOpeningMobileDetail = false; }); marketDetailOpen = true; window.history.pushState({ marketView: 'detail' }, '', window.location.href); };";
|
||||
echo " const deleteMarketOccurrence = async (submit) => { if (submit.disabled) { return; } const card = submit.closest('[data-shared-editable-action-card]'); const confirmation = Array.from(card?.children || []).find((child) => child.matches('[data-shared-editable-card-delete-confirmation]')); const input = confirmation?.querySelector('[data-shared-editable-card-delete-input]'); const status = confirmation?.querySelector('[data-market-delete-status]'); const occurrenceId = card?.dataset.marketDeleteOccurrenceId || ''; if (!card || !input || input.value !== 'DELETE' || !occurrenceId) { return; } if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'delete', id: occurrenceId, confirmation: input.value }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Löschen fehlgeschlagen.'); } closeMarketDrawer(); if (marketDetailOpen) { restoreMarketList(); } await loadFragment({ market_search: getSearchValue(), market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getCurrentLimit() }); } catch (error) { if (status) { status.textContent = marketEscape(error.message); } submit.disabled = false; submit.setAttribute('aria-disabled', 'false'); } };";
|
||||
echo " const deleteMarketDay = async (submit) => { if (submit.disabled) { return; } const card = submit.closest('[data-shared-editable-action-card]'); const input = card?.querySelector('[data-shared-editable-card-delete-input]'); const status = card?.querySelector('[data-market-day-delete-status]'); const dayId = card?.dataset.marketDayDeleteId || ''; if (!card || !input || input.value !== 'DELETE' || !dayId) { return; } if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'delete_day', id: dayId, confirmation: input.value }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Löschen fehlgeschlagen.'); } const trigger = contentRoot.querySelector('[data-market-drawer-open][aria-expanded=\"true\"]'); if (trigger) { await openMarketDrawer(trigger); } } catch (error) { if (status) { status.textContent = marketEscape(error.message); } submit.disabled = false; submit.setAttribute('aria-disabled', 'false'); } };";
|
||||
echo " const marketDayDeleteObserver = new MutationObserver(() => enhanceMarketDayDeleteControls(contentRoot)); marketDayDeleteObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
echo " const municipalityFieldObserver = new MutationObserver(() => initializeMunicipalityFields(contentRoot)); municipalityFieldObserver.observe(contentRoot, { childList: true, subtree: true }); initializeMunicipalityFields(contentRoot);";
|
||||
echo " const municipalityReadObserver = new MutationObserver(() => syncMunicipalityReadValue(contentRoot)); municipalityReadObserver.observe(contentRoot, { childList: true, subtree: true }); syncMunicipalityReadValue(contentRoot);";
|
||||
echo " if (!marketsBindingsInstalled) {";
|
||||
echo " marketsBindingsInstalled = true;";
|
||||
echo " document.addEventListener('click', (event) => { const dayAdd = event.target.closest('[data-market-day-add]'); if (dayAdd) { event.preventDefault(); addMarketDayDraft(); return; } const editButton = event.target.closest('[data-shared-editable-card-edit]'); if (editButton) { window.setTimeout(() => syncMarketDayAddButton(contentRoot), 0); return; } const cancelButton = event.target.closest('[data-shared-editable-card-cancel]'); if (cancelButton) { if (cancelButton.closest('[data-market-edit-form][data-market-edit-draft=\"true\"]')) { window.setTimeout(discardMarketDayDraft, 0); } else { window.setTimeout(() => syncMarketDayAddButton(contentRoot), 0); } } });";
|
||||
echo " document.addEventListener('click', (event) => { const deleteDaySubmit = event.target.closest('[data-market-day-delete-submit]'); if (deleteDaySubmit) { void deleteMarketDay(deleteDaySubmit); return; } const createButton = event.target.closest('[data-market-occurrence-create]'); if (createButton) { event.preventDefault(); void openNewMarketDrawer(); return; } if (event.target.closest('[data-market-create-cancel]')) { closeMarketDrawer(); } });";
|
||||
echo " document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-market-create-form]'); if (!form) { return; } event.preventDefault(); event.stopImmediatePropagation(); const status = form.querySelector('[data-market-create-status]'); const fields = Object.fromEntries(new FormData(form).entries()); if (status) { status.textContent = 'Speichern läuft …'; } try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'create', fields }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } const detailResponse = await fetch('/api/market-occurrence.php?id=' + encodeURIComponent(result.occurrence_id), { credentials: 'same-origin' }); const payload = await detailResponse.json(); if (!detailResponse.ok || !payload.ok) { throw new Error(payload.error || 'Die neue Durchführung konnte nicht geladen werden.'); } const drawer = getMarketDrawer(); const title = drawer?.querySelector('[data-market-drawer-title]'); const content = drawer?.querySelector('[data-market-drawer-content]'); if (drawer) { drawer.dataset.marketPayload = JSON.stringify(payload); } if (title) { title.textContent = marketDrawerTitle(payload); } if (content) { content.innerHTML = renderMarketDetailWithActions(payload); applyMarketTabs(content); syncMarketDayAddButton(content); } } catch (error) { if (status) { status.textContent = marketEscape(error.message); } } });";
|
||||
echo " document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-market-edit-form][data-market-edit-draft=\"true\"]'); if (!form) { return; } event.preventDefault(); event.stopImmediatePropagation(); const status = form.querySelector('[data-market-edit-status]'); const fields = Object.fromEntries(new FormData(form).entries()); if (status) { status.textContent = 'Speichern läuft …'; } try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'create_day', id: form.dataset.marketOccurrenceId, fields }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } const trigger = contentRoot.querySelector('[data-market-drawer-open][aria-expanded=\"true\"]'); if (trigger) { await openMarketDrawer(trigger); } } catch (error) { if (status) { status.textContent = marketEscape(error.message); } } });";
|
||||
echo " document.addEventListener('click', (event) => { const deleteSubmit = event.target.closest('[data-market-delete-submit]'); if (deleteSubmit) { void deleteMarketOccurrence(deleteSubmit); return; } const sortButton = event.target.closest('[data-markets-sort-button]'); if (sortButton) { event.preventDefault(); void loadFragment({ market_search: getSearchValue(), market_sort: sortButton.dataset.marketsSortColumn || getSortColumn(), market_dir: sortButton.dataset.marketsSortDirection || getSortDirection(), market_limit: getPageSize() }); return; } const loadMore = event.target.closest('[data-markets-load-more-trigger]'); if (loadMore) { event.preventDefault(); void loadFragment({ market_search: getSearchValue(), market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getCurrentLimit() + getPageSize() }); return; } const trigger = event.target.closest('[data-market-drawer-open]'); if (trigger) { event.preventDefault(); if (window.matchMedia('(max-width: 767px)').matches && !marketOpeningMobileDetail) { openMarketMobileDetail(trigger); } else { void openMarketDrawer(trigger); } return; } if (event.target.closest('[data-market-drawer-close]')) { closeMarketDrawer(); return; } if (event.target.closest('[data-market-detail-back]')) { if (window.history.state?.marketView === 'detail') { window.history.back(); } else { restoreMarketList(); } return; } });";
|
||||
echo " document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-market-edit-form]'); if (!form) { return; } event.preventDefault(); const status = form.querySelector('[data-market-edit-status]'); const fields = Object.fromEntries(new FormData(form).entries()); const occurrenceId = form.dataset.marketEditId || ''; if (status) { status.textContent = 'Speichern läuft …'; } try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ scope: form.dataset.marketEditScope, id: occurrenceId, fields }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } await loadFragment({ market_search: getSearchValue(), market_sort: getSortColumn(), market_dir: getSortDirection(), market_limit: getCurrentLimit() }); const refreshedTrigger = contentRoot.querySelector('[data-market-drawer-open][data-market-occurrence-id=\"' + occurrenceId + '\"]'); if (refreshedTrigger) { await openMarketDrawer(refreshedTrigger); } } catch (error) { if (status) { status.textContent = marketEscape(error.message); } } });";
|
||||
echo " document.addEventListener('click', (event) => { if (event.target.closest('[data-market-drawer-backdrop]')) { closeMarketDrawer(); } });";
|
||||
echo " document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { closeMarketDrawer(); restoreMarketList(); } });";
|
||||
echo " window.addEventListener('popstate', () => { if (marketDetailOpen) { restoreMarketList(); } });";
|
||||
echo " }";
|
||||
echo " const normalizeOccurrenceOverviewForm = (root) => { root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=overview]').forEach((form) => { if (form.dataset.marketOccurrenceFieldsNormalized === 'true') { return; } const organizerSelect = form.querySelector('[name=organization_contact_id]'); const contactSelect = form.querySelector('[name=contact_person_id]'); const organizerField = organizerSelect?.closest('.sg-form-sections-card__field-group'); const contactField = contactSelect?.closest('.sg-form-sections-card__field-group'); if (organizerField && organizerSelect) { const value = organizerSelect.selectedOptions[0]?.textContent || 'Nicht hinterlegt'; organizerField.innerHTML = '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Veranstalter</span><input class=\"sg-interaction-element sg-input-single-line sg-input-single-line--disabled sg-form-disabled\" type=\"text\" name=\"organizer_name\" value=\"' + marketEscape(value) + '\" disabled aria-disabled=\"true\"></label>'; } if (contactField && contactSelect) { const value = contactSelect.selectedOptions[0]?.textContent || ''; contactField.innerHTML = '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Kontaktperson</span><input class=\"sg-interaction-element sg-input-single-line\" type=\"text\" name=\"contact_person_name\" value=\"' + marketEscape(value) + '\"></label>'; } form.dataset.marketOccurrenceFieldsNormalized = 'true'; }); };";
|
||||
echo " const marketOccurrenceFieldObserver = new MutationObserver(() => normalizeOccurrenceOverviewForm(contentRoot)); marketOccurrenceFieldObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
echo " const normalizeMarketSimpleCostFields = (root) => { root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation]').forEach((form) => { if (form.dataset.marketSimpleCostsNormalized === 'true') { return; } const drawer = getMarketDrawer(); let payload = {}; try { payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : {}; } catch (error) {} const item = Array.isArray(payload.occurrence?.cost_items) ? payload.occurrence.cost_items[0] : null; const total = form.querySelector('[name=gross_cost]'); const text = form.querySelector('[name=cost_items_text]'); const totalLabel = total?.closest('.sg-labeled-input-row')?.querySelector('.sg-label'); const textLabel = text?.closest('.sg-labeled-input-row')?.querySelector('.sg-label'); if (total) { total.disabled = false; total.removeAttribute('disabled'); total.type = 'number'; total.step = '0.01'; total.min = '0'; total.required = true; total.classList.remove('sg-input-single-line--disabled', 'sg-form-disabled'); total.value = payload.occurrence?.cost_summary?.gross_amount ?? item?.gross_amount ?? ''; } if (text) { text.disabled = false; text.removeAttribute('disabled'); text.classList.remove('sg-form-disabled'); text.value = payload.occurrence?.cost_items_text ?? item?.description ?? ''; } if (totalLabel) { totalLabel.textContent = 'Gesamtkosten'; } if (textLabel) { textLabel.textContent = 'Kostenpositionen'; } form.dataset.marketSimpleCostsNormalized = 'true'; }); };";
|
||||
echo " const marketSimpleCostFieldsObserver = new MutationObserver(() => normalizeMarketSimpleCostFields(contentRoot)); marketSimpleCostFieldsObserver.observe(contentRoot, { childList: true, subtree: true }); normalizeMarketSimpleCostFields(contentRoot);";
|
||||
echo " const normalizeMarketTaxFields = (root) => { root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation]').forEach((form) => { if (form.dataset.marketTaxFieldsNormalized === 'true') { return; } const total = form.querySelector('[name=gross_cost]'); const totalGroup = total?.closest('.sg-form-sections-card__field-group'); if (!totalGroup) { return; } const drawer = getMarketDrawer(); let payload = {}; try { payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : {}; } catch (error) {} const item = Array.isArray(payload.occurrence?.cost_items) ? payload.occurrence.cost_items[0] : null; const taxStatusGroup = document.createElement('div'); taxStatusGroup.className = 'sg-form-sections-card__field-group'; taxStatusGroup.innerHTML = marketEditField('tax_status', 'Steuerstatus', 'select', item?.tax_status || 'taxable', [['taxable', 'Steuerpflichtig'], ['tax_free', 'Steuerfrei']], true); const taxRateGroup = document.createElement('div'); taxRateGroup.className = 'sg-form-sections-card__field-group'; taxRateGroup.innerHTML = marketEditField('tax_rate', 'Steuersatz', 'number', item?.tax_rate ?? '8.10', [], true); totalGroup.after(taxStatusGroup, taxRateGroup); const taxStatus = taxStatusGroup.querySelector('[name=tax_status]'); const taxRate = taxRateGroup.querySelector('[name=tax_rate]'); const sync = () => { const taxFree = taxStatus?.value === 'tax_free'; if (taxFree && taxRate) { taxRate.value = '0.00'; } if (taxRate) { taxRate.disabled = taxFree; taxRate.setAttribute('aria-disabled', String(taxFree)); } }; taxStatus?.addEventListener('change', sync); sync(); form.dataset.marketTaxFieldsNormalized = 'true'; }); };";
|
||||
echo " const marketTaxFieldsObserver = new MutationObserver(() => normalizeMarketTaxFields(contentRoot)); marketTaxFieldsObserver.observe(contentRoot, { childList: true, subtree: true }); normalizeMarketTaxFields(contentRoot);";
|
||||
echo " const marketTaxRateAttributesObserver = new MutationObserver(() => { contentRoot.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation] [name=tax_rate]').forEach((input) => { input.step = '0.01'; input.min = '0'; }); }); marketTaxRateAttributesObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
echo " const marketPaidAmountObserver = new MutationObserver(() => { contentRoot.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation] [name=paid_amount]').forEach((input) => { input.disabled = false; input.removeAttribute('disabled'); input.type = 'number'; input.step = '0.01'; input.min = '0'; input.classList.remove('sg-input-single-line--disabled', 'sg-form-disabled'); }); }); marketPaidAmountObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
echo " const marketPaidAmountPositionObserver = new MutationObserver(() => { contentRoot.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation]').forEach((form) => { const paidGroup = form.querySelector('[name=paid_amount]')?.closest('.sg-form-sections-card__field-group'); const taxGroup = form.querySelector('[name=tax_cost]')?.closest('.sg-form-sections-card__field-group'); if (paidGroup && taxGroup && taxGroup.nextElementSibling !== paidGroup) { taxGroup.after(paidGroup); } }); }); marketPaidAmountPositionObserver.observe(contentRoot, { childList: true, subtree: true });";
|
||||
echo " const syncMarketSimpleCostTotals = (form) => { const gross = Number.parseFloat(form.querySelector('[name=gross_cost]')?.value || '0'); const rate = form.querySelector('[name=tax_status]')?.value === 'tax_free' ? 0 : Number.parseFloat(form.querySelector('[name=tax_rate]')?.value || '0'); const tax = Number.isFinite(gross) && Number.isFinite(rate) ? Math.round((gross - gross / (1 + rate / 100)) * 100) / 100 : 0; const net = Number.isFinite(gross) ? Math.round((gross - tax) * 100) / 100 : 0; const format = (value) => 'CHF ' + value.toFixed(2); const netField = form.querySelector('[name=net_cost]'); const taxField = form.querySelector('[name=tax_cost]'); if (netField) { netField.value = format(net); } if (taxField) { taxField.value = format(tax); } };";
|
||||
echo " document.addEventListener('input', (event) => { const field = event.target.closest('[data-market-edit-form][data-market-edit-scope=participation] [name=gross_cost], [data-market-edit-form][data-market-edit-scope=participation] [name=tax_rate]'); if (field) { syncMarketSimpleCostTotals(field.closest('[data-market-edit-form]')); } });";
|
||||
echo " document.addEventListener('change', (event) => { const field = event.target.closest('[data-market-edit-form][data-market-edit-scope=participation] [name=tax_status]'); if (field) { syncMarketSimpleCostTotals(field.closest('[data-market-edit-form]')); } });";
|
||||
echo " bindTable();";
|
||||
echo "}";
|
||||
echo "function initMarketDefinitionsBindings() {";
|
||||
echo " const contentRoot = document.querySelector('[data-left-navigation-content-body]');";
|
||||
echo " const definitionContent = contentRoot?.querySelector('[data-market-definitions-content]');";
|
||||
echo " const drawer = definitionContent?.querySelector('[data-market-definition-drawer]');";
|
||||
echo " const readCard = drawer?.querySelector('[data-market-definition-read-card]');";
|
||||
echo " const card = drawer?.querySelector('[data-market-definition-create-card]');";
|
||||
echo " const form = drawer?.querySelector('[data-market-definition-form]');";
|
||||
echo " const editButton = drawer?.querySelector('[data-market-definition-edit]');";
|
||||
echo " if (!contentRoot || !definitionContent || !drawer || !readCard || !card || !form || !editButton || definitionContent.dataset.marketDefinitionsBound === 'true') { return; }";
|
||||
echo " definitionContent.dataset.marketDefinitionsBound = 'true';";
|
||||
echo " let activeMarket = null;";
|
||||
echo <<<JS
|
||||
const prepareMarketDefinitionDeleteCard = () => {
|
||||
const existingCard = drawer.querySelector('[data-market-definition-card]');
|
||||
if (!existingCard || drawer.querySelector('[data-market-definition-delete-id]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stage = document.createElement('div');
|
||||
stage.className = 'sg-delete-confirmation-pattern__stage sg-delete-confirmation-pattern__host';
|
||||
stage.dataset.pattern = 'overlay-card';
|
||||
stage.dataset.dialogOpen = 'false';
|
||||
const target = document.createElement('div');
|
||||
target.className = 'sg-delete-confirmation-pattern__target';
|
||||
target.dataset.sharedEditableActionCard = '';
|
||||
target.dataset.sharedEditableDeleteToken = 'DELETE';
|
||||
target.dataset.marketDefinitionDeleteId = '';
|
||||
target.dataset.marketDefinitionCard = '';
|
||||
|
||||
const titleCard = document.createElement('article');
|
||||
titleCard.className = 'sg-card sg-card--content-card market-delete-confirmation-card';
|
||||
const header = document.createElement('header');
|
||||
header.className = 'sg-card-segment sg-card-segment--header sg-card-segment--gray sg-object-card__header';
|
||||
const heading = document.createElement('h2');
|
||||
heading.className = 'sg-heading-h2 market-delete-confirmation-card__title';
|
||||
heading.dataset.marketDefinitionReadTitle = '';
|
||||
heading.dataset.marketDefinitionDrawerTitle = '';
|
||||
heading.textContent = 'Markt';
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'sg-component-row';
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.className = 'sg-interaction-element sg-button sg-button--warning';
|
||||
deleteButton.type = 'button';
|
||||
deleteButton.dataset.sharedEditableCardDelete = '';
|
||||
deleteButton.dataset.marketDefinitionDelete = '';
|
||||
deleteButton.textContent = 'Löschen';
|
||||
actions.append(deleteButton);
|
||||
header.append(heading, actions);
|
||||
titleCard.append(header);
|
||||
existingCard.querySelector('[data-market-definition-read-title]')?.remove();
|
||||
existingCard.removeAttribute('data-shared-editable-action-card');
|
||||
|
||||
const confirmation = document.createElement('article');
|
||||
confirmation.className = 'sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card';
|
||||
confirmation.setAttribute('aria-label', 'Löschbestätigung');
|
||||
confirmation.setAttribute('role', 'dialog');
|
||||
confirmation.setAttribute('aria-modal', 'true');
|
||||
confirmation.dataset.overlayDialog = 'delete';
|
||||
confirmation.dataset.sharedEditableCardDeleteConfirmation = '';
|
||||
confirmation.hidden = true;
|
||||
confirmation.innerHTML = '<div class="sg-card-segment sg-card-segment--body sg-delete-confirmation-pattern__body"><p class="sg-body sg-delete-confirmation-pattern__text"><strong>Möchtest du diesen Markt wirklich löschen?</strong></p><p class="sg-body sg-delete-confirmation-pattern__text">Du kannst das nicht rückgängig machen. Bestätige durch Eingabe von <span class="sg-delete-confirmation-pattern__code">DELETE</span>.</p><label class="sg-labeled-input-row sg-delete-confirmation-pattern__input-row"><span class="sg-label">Bestätigung</span><input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" placeholder="DELETE" aria-label="Löschbestätigung durch DELETE" data-shared-editable-card-delete-input></label><p class="sg-body" data-market-definition-delete-status aria-live="polite"></p><div class="sg-delete-confirmation-pattern__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" data-shared-editable-card-delete-cancel>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process sg-button--process-inactive" type="button" disabled aria-disabled="true" data-shared-editable-card-delete-submit data-market-definition-delete-submit>Löschen</button></div></div>';
|
||||
target.append(titleCard, existingCard, confirmation);
|
||||
stage.append(target);
|
||||
drawer.querySelector('[data-market-definition-drawer-content]')?.append(stage);
|
||||
};
|
||||
JS;
|
||||
echo " prepareMarketDefinitionDeleteCard();";
|
||||
echo " const sharedCard = drawer.querySelector('[data-market-definition-card]');";
|
||||
echo " if (window.SharedEditableActionCard) { window.SharedEditableActionCard.init(definitionContent); }";
|
||||
echo " const setDrawerState = (open) => { drawer.dataset.open = String(open); drawer.setAttribute('aria-hidden', String(!open)); if (activeMarket?.trigger) { activeMarket.trigger.setAttribute('aria-expanded', String(open)); } const backdrop = definitionContent.querySelector('[data-market-definition-drawer-backdrop]'); if (backdrop) { backdrop.dataset.open = String(open); backdrop.setAttribute('aria-hidden', String(!open)); } document.body.classList.toggle('sg-card-list-page-drawer-open', open); };";
|
||||
echo " const resetForm = () => { form.reset(); const status = form.querySelector('[data-market-definition-status]'); if (status) { status.textContent = ''; } syncNewOrganizerFields(); };";
|
||||
echo " const syncNewOrganizerFields = () => { const select = form.querySelector('[data-market-definition-organizer]'); const fields = form.querySelector('[data-market-definition-new-organizer]'); if (!select || !fields) { return; } fields.hidden = select.value !== '__new__'; };";
|
||||
echo " const closeDrawer = () => { resetForm(); if (sharedCard && window.SharedEditableActionCard) { window.SharedEditableActionCard.resetDeleteConfirmation(sharedCard); window.SharedEditableActionCard.setEditMode(sharedCard, false); } activeMarket = null; setDrawerState(false); };";
|
||||
echo " const deleteMarketDefinition = async (submit) => { if (submit.disabled || !activeMarket?.id) { return; } const card = submit.closest('[data-shared-editable-action-card]'); const input = card?.querySelector('[data-shared-editable-card-delete-input]'); const status = card?.querySelector('[data-market-definition-delete-status]'); if (!card || !input || input.value !== 'DELETE') { return; } if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); try { const response = await fetch('/api/market-definition.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': definitionContent.dataset.marketDefinitionsCsrfToken || '' }, body: JSON.stringify({ action: 'delete', id: activeMarket.id, confirmation: input.value }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Löschen fehlgeschlagen.'); } const row = activeMarket.trigger.closest('[data-market-definition-row]'); row?.remove(); closeDrawer(); } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Löschen fehlgeschlagen.'; } submit.disabled = false; submit.setAttribute('aria-disabled', 'false'); } };";
|
||||
echo " const showReadCard = (trigger) => { activeMarket = { id: trigger.dataset.marketDefinitionId || '', trigger }; drawer.querySelector('[data-market-definition-read-title]').textContent = trigger.dataset.marketDefinitionName || 'Markt'; readCard.querySelector('[data-market-definition-read-name]').textContent = trigger.dataset.marketDefinitionName || 'Nicht hinterlegt'; readCard.querySelector('[data-market-definition-read-category]').textContent = trigger.dataset.marketDefinitionCategoryName || 'Nicht hinterlegt'; readCard.querySelector('[data-market-definition-read-organizer]').textContent = trigger.dataset.marketDefinitionOrganizerName || 'Nicht hinterlegt'; readCard.querySelector('[data-market-definition-read-website]').textContent = trigger.dataset.marketDefinitionWebsite || 'Nicht hinterlegt'; const title = form.querySelector('[data-market-definition-form-title]'); if (title) { title.textContent = 'Markt bearbeiten'; } const name = form.querySelector('[data-market-definition-name]'); const category = form.querySelector('[data-market-definition-category]'); const organizer = form.querySelector('[data-market-definition-organizer]'); const website = form.querySelector('[data-market-definition-website]'); if (name) { name.value = trigger.dataset.marketDefinitionName || ''; } if (category) { category.value = trigger.dataset.marketDefinitionCategoryId || ''; } if (organizer) { organizer.value = trigger.dataset.marketDefinitionOrganizerId || ''; } if (website) { website.value = trigger.dataset.marketDefinitionWebsite === 'Nicht hinterlegt' ? '' : (trigger.dataset.marketDefinitionWebsite || ''); } syncNewOrganizerFields(); if (sharedCard && window.SharedEditableActionCard) { window.SharedEditableActionCard.setEditMode(sharedCard, false); } setDrawerState(true); };";
|
||||
echo " definitionContent.addEventListener('click', (event) => { const trigger = event.target.closest('[data-market-definition-open]'); if (!trigger || !definitionContent.contains(trigger)) { return; } event.preventDefault(); event.stopPropagation(); showReadCard(trigger); }, true);";
|
||||
echo " definitionContent.addEventListener('click', (event) => { const deleteSubmit = event.target.closest('[data-market-definition-delete-submit]'); if (deleteSubmit) { void deleteMarketDefinition(deleteSubmit); return; } const trigger = event.target.closest('[data-market-definition-open]'); if (trigger) { const deleteButton = drawer.querySelector('[data-market-definition-delete]'); if (deleteButton) { deleteButton.hidden = false; } } if (event.target.closest('[data-market-definition-create]')) { const deleteButton = drawer.querySelector('[data-market-definition-delete]'); if (deleteButton) { deleteButton.hidden = true; } } });";
|
||||
echo " const openEditCard = () => { if (!activeMarket) { return; } const title = form.querySelector('[data-market-definition-form-title]'); if (title) { title.textContent = 'Markt bearbeiten'; } const name = form.querySelector('[data-market-definition-name]'); const category = form.querySelector('[data-market-definition-category]'); const organizer = form.querySelector('[data-market-definition-organizer]'); const website = form.querySelector('[data-market-definition-website]'); if (name) { name.value = activeMarket.trigger.dataset.marketDefinitionName || ''; } if (category) { category.value = activeMarket.trigger.dataset.marketDefinitionCategoryId || ''; } if (organizer) { organizer.value = activeMarket.trigger.dataset.marketDefinitionOrganizerId || ''; } if (website) { website.value = activeMarket.trigger.dataset.marketDefinitionWebsite === 'Nicht hinterlegt' ? '' : (activeMarket.trigger.dataset.marketDefinitionWebsite || ''); } syncNewOrganizerFields(); };";
|
||||
echo " editButton.addEventListener('click', (event) => { event.preventDefault(); openEditCard(); });";
|
||||
echo " form.querySelector('[data-market-definition-organizer]')?.addEventListener('change', syncNewOrganizerFields);";
|
||||
echo " const appendMarketRow = (market) => { const table = contentRoot.querySelector('[data-market-definitions-table]'); if (!table) { return; } const row = document.createElement('div'); row.className = 'sg-card-segment sg-card-segment--body sg-large-table__row'; row.setAttribute('role', 'row'); row.dataset.componentPart = 'large-table-row'; row.dataset.marketDefinitionRow = ''; row.dataset.marketDefinitionId = String(market.id || ''); const category = form.querySelector('[data-market-definition-category]')?.selectedOptions[0]?.textContent || ''; const organizer = form.querySelector('[data-market-definition-organizer]')?.selectedOptions[0]?.textContent || ''; const website = market.website_url || 'Nicht hinterlegt'; const cells = document.createElement('div'); cells.className = 'sg-large-table__cell'; const trigger = document.createElement('a'); trigger.className = 'sg-hyperlink'; trigger.href = '#'; trigger.setAttribute('aria-haspopup', 'dialog'); trigger.setAttribute('aria-expanded', 'false'); trigger.dataset.marketDefinitionOpen = ''; trigger.dataset.marketDefinitionId = String(market.id || ''); trigger.dataset.marketDefinitionName = market.name || ''; trigger.dataset.marketDefinitionCategoryId = String(market.category_id || ''); trigger.dataset.marketDefinitionCategoryName = category; trigger.dataset.marketDefinitionOrganizerId = String(market.organizer_id || ''); trigger.dataset.marketDefinitionOrganizerName = organizer; trigger.dataset.marketDefinitionWebsite = website; trigger.textContent = market.name || ''; cells.appendChild(trigger); row.appendChild(cells); [category, organizer, website, '0', 'Aktiv'].forEach((value) => { const cell = document.createElement('div'); cell.className = 'sg-large-table__cell'; cell.setAttribute('role', 'cell'); cell.textContent = value; row.appendChild(cell); }); table.appendChild(row); };";
|
||||
echo " document.addEventListener('click', (event) => { if (event.target.closest('[data-market-definition-create]')) { event.preventDefault(); activeMarket = null; resetForm(); const title = form.querySelector('[data-market-definition-form-title]'); if (title) { title.textContent = 'Neuen Markt anlegen'; } if (sharedCard && window.SharedEditableActionCard) { window.SharedEditableActionCard.setEditMode(sharedCard, true); } setDrawerState(true); form.querySelector('[data-market-definition-name]')?.focus(); return; } if (event.target.closest('[data-market-definition-edit]')) { event.preventDefault(); openEditCard(); return; } if (event.target.closest('[data-market-definition-cancel]')) { event.preventDefault(); if (!activeMarket) { closeDrawer(); } return; } if (event.target.closest('[data-market-definition-drawer-backdrop]')) { event.preventDefault(); closeDrawer(); } });";
|
||||
echo " document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && drawer.dataset.open === 'true') { closeDrawer(); } });";
|
||||
echo " form.addEventListener('submit', async (event) => { event.preventDefault(); const status = form.querySelector('[data-market-definition-status]'); const submit = form.querySelector('[type=submit]'); const fields = Object.fromEntries(new FormData(form).entries()); const isUpdate = Boolean(activeMarket?.id); if (fields.organizer_id === '__new__' && !String(fields.organizer_name || '').trim()) { if (status) { status.textContent = 'Name des neuen Veranstalters ist erforderlich.'; } return; } if (status) { status.textContent = 'Speichern läuft …'; } if (submit) { submit.disabled = true; } try { const response = await fetch('/api/market-definition.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': definitionContent.dataset.marketDefinitionsCsrfToken || '' }, body: JSON.stringify({ action: isUpdate ? 'update' : 'create', id: isUpdate ? activeMarket.id : null, fields }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } if (isUpdate) { const trigger = activeMarket.trigger; const category = form.querySelector('[data-market-definition-category]')?.selectedOptions[0]; const organizer = form.querySelector('[data-market-definition-organizer]')?.selectedOptions[0]; const website = result.market.website_url || 'Nicht hinterlegt'; trigger.textContent = result.market.name; trigger.dataset.marketDefinitionName = result.market.name; trigger.dataset.marketDefinitionCategoryId = String(result.market.category_id); trigger.dataset.marketDefinitionCategoryName = category?.textContent || ''; trigger.dataset.marketDefinitionOrganizerId = String(result.market.organizer_id); trigger.dataset.marketDefinitionOrganizerName = organizer?.textContent || ''; trigger.dataset.marketDefinitionWebsite = website; showReadCard(trigger); } else { appendMarketRow(result.market); closeDrawer(); } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'; } } finally { if (submit) { submit.disabled = false; } } });";
|
||||
echo "}";
|
||||
echo "function initBestellungenBindings() {";
|
||||
echo " const contentRoot = document.querySelector('[data-left-navigation-content-body]');";
|
||||
echo " if (!contentRoot) { return; }";
|
||||
@@ -980,12 +1395,12 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const orderId = trigger.dataset.orderId || '';";
|
||||
echo " const formatAmount = (value, currency) => value === null || value === undefined ? 'Nicht vorhanden' : currency + ' ' + Number(value).toFixed(2);";
|
||||
echo " const formatDate = (value) => value ? new Date(value).toLocaleString('de-CH', { dateStyle: 'short', timeStyle: 'short' }) : 'Nicht vorhanden';";
|
||||
echo " const value = (label, content) => '<p class=\"sg-body\"><strong>' + escapeHtml(label) + ':</strong> ' + escapeHtml(content || 'Nicht vorhanden') + '</p>';";
|
||||
echo " const value = (label, content) => '<div class=\"market-detail-row\"><dt>' + escapeHtml(label) + '</dt><dd>' + escapeHtml(content || 'Nicht vorhanden') + '</dd></div>';";
|
||||
echo " const renderCards = (order) => {";
|
||||
echo " const c = order.customer || {}; const a = order.amounts || {}; const address = [[c.street, c.house_number].filter(Boolean).join(' '), [c.zip, c.city].filter(Boolean).join(' '), c.country_name].filter(Boolean).join('<br>');";
|
||||
echo " const lines = (order.lines || []).map((line) => '<div class=\"sg-card-segment sg-card-segment--body\"><p class=\"sg-body\"><strong>' + escapeHtml(line.item_name) + '</strong> · ' + escapeHtml(line.item_code) + ' · Menge: ' + escapeHtml(String(line.qty)) + '</p>' + value('Positionsstatus', line.line_status) + value('Einzelpreis', formatAmount(line.unit_price, a.currency || 'CHF')) + value('Positionssumme', formatAmount(line.line_total, a.currency || 'CHF')) + '</div>').join('');";
|
||||
echo " const allocations = (order.lines || []).flatMap((line) => (line.product_allocations || []).map((item) => '<div class=\"sg-body\"><strong>' + escapeHtml(item.product_name) + ':</strong> ' + escapeHtml(item.lot_number || 'Nicht zugeordnet') + ' · ' + escapeHtml(item.allocated_qty === null ? 'Nicht zugeordnet' : String(item.allocated_qty)) + ' · ' + escapeHtml(item.allocation_status || 'Nicht vorhanden') + (item.lot_status === 'virtual_shortage' ? ' · Virtuelle Fehlbestandscharge' : '') + '</div>')).join('');";
|
||||
echo " return '<article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Bestellübersicht\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Bestellübersicht</div></header><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\">' + value('Bestellnummer', order.external_ref) + value('Bestelldatum', formatDate(order.order_date)) + value('Verkaufskanal', order.order_source) + value('Bestellstatus', order.order_status) + value('Zahlungsstatus', order.payment_status) + value('Zahlungsart', order.payment_method) + value('Versandart', order.shipping_method) + '<p class=\"sg-body\"><strong>Kunde:</strong> ' + escapeHtml([c.first_name, c.last_name].filter(Boolean).join(' ') || 'Nicht vorhanden') + '</p><p class=\"sg-body\"><strong>Lieferadresse:</strong><br>' + address + '</p>' + value('Netto', formatAmount(a.net, a.currency || 'CHF')) + value('Versand', formatAmount(a.shipping, a.currency || 'CHF')) + value('Rabatt', formatAmount(a.discount, a.currency || 'CHF')) + value('Steuer', formatAmount(a.tax, a.currency || 'CHF')) + value('Gesamtsumme', formatAmount(a.total, a.currency || 'CHF')) + '</div></article><article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Bestellte Artikel\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Bestellte Artikel</div></header>' + (lines || '<div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\"><p class=\"sg-body\">Keine Artikel vorhanden.</p></div>') + '</article><article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Chargenzuordnung\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Chargenzuordnung</div></header><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\">' + (allocations || '<p class=\"sg-body\">Keine Chargenzuordnung vorhanden.</p>') + '</div></article>';";
|
||||
echo " const c = order.customer || {}; const a = order.amounts || {}; const address = [[c.street, c.house_number].filter(Boolean).join(' '), [c.zip, c.city].filter(Boolean).join(' '), c.country_name].filter(Boolean).map(escapeHtml).join('<br>');";
|
||||
echo " const lines = (order.lines || []).map((line) => '<div class=\"sg-card-segment sg-card-segment--body\"><dl class=\"market-detail-grid\">' + value(line.item_name, [line.item_code, 'Menge: ' + String(line.qty)].filter(Boolean).join(' · ')) + value('Positionsstatus', line.line_status) + value('Einzelpreis', formatAmount(line.unit_price, a.currency || 'CHF')) + value('Positionssumme', formatAmount(line.line_total, a.currency || 'CHF')) + '</dl></div>').join('');";
|
||||
echo " const allocations = (order.lines || []).flatMap((line) => (line.product_allocations || []).map((item) => '<dl class=\"market-detail-grid\">' + value(item.product_name, [item.lot_number || 'Nicht zugeordnet', item.allocated_qty === null ? 'Nicht zugeordnet' : String(item.allocated_qty), item.allocation_status || 'Nicht vorhanden', item.lot_status === 'virtual_shortage' ? 'Virtuelle Fehlbestandscharge' : ''].filter(Boolean).join(' · ')) + '</dl>')).join('');";
|
||||
echo " return '<article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Bestellübersicht\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Bestellübersicht</div></header><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\"><dl class=\"market-detail-grid\">' + value('Bestellnummer', order.external_ref) + value('Bestelldatum', formatDate(order.order_date)) + value('Verkaufskanal', order.order_source) + value('Bestellstatus', order.order_status) + value('Zahlungsstatus', order.payment_status) + value('Zahlungsart', order.payment_method) + value('Versandart', order.shipping_method) + value('Kunde', [c.first_name, c.last_name].filter(Boolean).join(' ')) + '<div class=\"market-detail-row\"><dt>Lieferadresse</dt><dd>' + (address || 'Nicht vorhanden') + '</dd></div>' + value('Netto', formatAmount(a.net, a.currency || 'CHF')) + value('Versand', formatAmount(a.shipping, a.currency || 'CHF')) + value('Rabatt', formatAmount(a.discount, a.currency || 'CHF')) + value('Steuer', formatAmount(a.tax, a.currency || 'CHF')) + value('Gesamtsumme', formatAmount(a.total, a.currency || 'CHF')) + '</dl></div></article><article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Bestellte Artikel\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Bestellte Artikel</div></header>' + (lines || '<div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\"><p class=\"sg-body\">Keine Artikel vorhanden.</p></div>') + '</article><article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Chargenzuordnung\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\" data-component-part=\"card-header\"><div class=\"sg-strong sg-text-on-dark\">Chargenzuordnung</div></header><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\">' + (allocations || '<p class=\"sg-body\">Keine Chargenzuordnung vorhanden.</p>') + '</div></article>';";
|
||||
echo " };";
|
||||
echo " setDrawerState(true);";
|
||||
echo " if (drawerTitle) { drawerTitle.textContent = trigger.dataset.orderNumber ? 'Bestellung ' + trigger.dataset.orderNumber : 'Bestellung'; }";
|
||||
@@ -1240,6 +1655,10 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const houseNumberInput = form.querySelector('[data-otc-order-house-number]');";
|
||||
echo " const zipInput = form.querySelector('[data-otc-order-zip]');";
|
||||
echo " const cityInput = form.querySelector('[data-otc-order-city]');";
|
||||
echo " const marketLinkToggle = form.querySelector('[data-otc-market-link-toggle]');";
|
||||
echo " const marketLinkEnabledInput = form.querySelector('[data-otc-market-link-enabled]');";
|
||||
echo " const marketOccurrenceInput = form.querySelector('[data-otc-market-occurrence]');";
|
||||
echo " const marketDayInput = form.querySelector('[data-otc-market-day]');";
|
||||
echo " const submitBtn = overlay.querySelector('[data-otc-order-submit]');";
|
||||
echo " const errorEl = form.querySelector('[data-otc-order-error]');";
|
||||
echo " const successEl = form.querySelector('[data-otc-order-success]');";
|
||||
@@ -1250,6 +1669,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const confirmationQuantity = overlay.querySelector('[data-otc-confirmation-quantity]');";
|
||||
echo " const confirmationTotal = overlay.querySelector('[data-otc-confirmation-total]');";
|
||||
echo " const confirmationSubmit = overlay.querySelector('[data-otc-confirmation-submit]');";
|
||||
echo " const confirmationError = overlay.querySelector('[data-otc-confirmation-error]');";
|
||||
echo " let pendingOrderData = null;";
|
||||
echo " const openTriggers = () => document.querySelectorAll('[data-otc-order-open]');";
|
||||
echo " const setTriggerState = (expanded) => {";
|
||||
@@ -1257,6 +1677,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " };";
|
||||
echo " const clearError = () => {";
|
||||
echo " if (errorEl) { errorEl.textContent = ''; errorEl.classList.add('hidden'); }";
|
||||
echo " if (confirmationError) { confirmationError.textContent = ''; confirmationError.classList.add('hidden'); }";
|
||||
echo " };";
|
||||
echo " const clearSuccess = () => {";
|
||||
echo " if (successEl) { successEl.textContent = successDefaultText; successEl.classList.add('hidden'); }";
|
||||
@@ -1293,6 +1714,10 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " };";
|
||||
echo " const getInputValue = (input) => parseInt(input.value || '0', 10) || 0;";
|
||||
echo " const getFieldValue = (input) => (input ? input.value.trim() : '');";
|
||||
echo " const isMarketLinkEnabled = () => marketLinkToggle?.getAttribute('aria-checked') === 'true';";
|
||||
echo " const setDisabledSelectState = (select, disabled) => { if (!select) { return; } select.disabled = disabled; select.setAttribute('aria-disabled', String(disabled)); select.classList.toggle('sg-form-disabled', disabled); select.classList.toggle('sg-input-single-line--disabled', disabled); select.classList.toggle('sg-pulldown--disabled', disabled); select.classList.toggle('sg-form-inactive-selectable', !disabled); select.classList.toggle('sg-input-single-line--inactive-selectable', !disabled); };";
|
||||
echo " const syncMarketDayOptions = () => { if (!marketOccurrenceInput || !marketDayInput) { return; } const marketDefinitionId = marketOccurrenceInput.value; Array.from(marketDayInput.options).forEach((option, index) => { if (index === 0) { option.hidden = false; option.disabled = false; return; } const matches = marketDefinitionId !== '' && option.dataset.marketDefinitionId === marketDefinitionId; option.hidden = !matches; option.disabled = !matches; }); if (!marketDefinitionId || !Array.from(marketDayInput.options).some((option) => option.value === marketDayInput.value && !option.disabled)) { marketDayInput.value = ''; } setDisabledSelectState(marketDayInput, !isMarketLinkEnabled() || marketDefinitionId === ''); };";
|
||||
echo " const syncMarketLinkState = () => { const enabled = isMarketLinkEnabled(); if (marketLinkEnabledInput) { marketLinkEnabledInput.value = enabled ? '1' : '0'; } setDisabledSelectState(marketOccurrenceInput, !enabled); if (!enabled && marketOccurrenceInput) { marketOccurrenceInput.value = ''; } syncMarketDayOptions(); };";
|
||||
echo " const updatePaymentAddButton = () => { addPaymentButton.disabled = paymentRows.querySelectorAll('[data-otc-payment-row]').length >= paymentOptionValues.length; };";
|
||||
echo " const updateFormState = () => {";
|
||||
echo " const totalQty = productInputs.reduce((sum, input) => sum + getInputValue(input), 0);";
|
||||
@@ -1304,7 +1729,8 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " && !!getFieldValue(streetInput)";
|
||||
echo " && !!getFieldValue(houseNumberInput)";
|
||||
echo " && !!getFieldValue(zipInput)";
|
||||
echo " && !!getFieldValue(cityInput);";
|
||||
echo " && !!getFieldValue(cityInput)";
|
||||
echo " && (!isMarketLinkEnabled() || (!!getFieldValue(marketOccurrenceInput) && !!getFieldValue(marketDayInput)));";
|
||||
echo " let errorMsg = '';";
|
||||
echo " if (totalQty === 0) {";
|
||||
echo " errorMsg = 'Mindestens ein Produkt mit Menge > 0 erforderlich.';";
|
||||
@@ -1312,6 +1738,8 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " errorMsg = 'Bestelldatum auswählen.';";
|
||||
echo " } else if (!getFieldValue(firstNameInput) || !getFieldValue(lastNameInput) || !getFieldValue(streetInput) || !getFieldValue(houseNumberInput) || !getFieldValue(zipInput) || !getFieldValue(cityInput)) {";
|
||||
echo " errorMsg = 'Alle Rechnungsadress-Felder ausfüllen.';";
|
||||
echo " } else if (isMarketLinkEnabled() && (!getFieldValue(marketOccurrenceInput) || !getFieldValue(marketDayInput))) {";
|
||||
echo " errorMsg = 'Markt und Markttag auswählen.';";
|
||||
echo " }";
|
||||
echo " if (errorEl) {";
|
||||
echo " errorEl.textContent = errorMsg;";
|
||||
@@ -1328,6 +1756,8 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " updatePaymentAddButton();";
|
||||
echo " clearError();";
|
||||
echo " if (!preserveSuccess) { clearSuccess(); }";
|
||||
echo " if (marketLinkToggle) { marketLinkToggle.setAttribute('aria-checked', 'false'); marketLinkToggle.classList.remove('sg-form-active'); marketLinkToggle.classList.add('sg-checkbox-field--inactive-selectable'); }";
|
||||
echo " syncMarketLinkState();";
|
||||
echo " if (!preserveSuccess) { updateFormState(); }";
|
||||
echo " };";
|
||||
echo " const closeConfirmation = () => {";
|
||||
@@ -1394,6 +1824,9 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " addPaymentButton.addEventListener('click', addPaymentRow);";
|
||||
echo " bindPaymentRow(paymentRows.querySelector('[data-otc-payment-row]'));";
|
||||
echo " updatePaymentAddButton();";
|
||||
echo " marketLinkToggle?.addEventListener('click', (event) => { event.stopImmediatePropagation(); const enabled = marketLinkToggle.getAttribute('aria-checked') !== 'true'; marketLinkToggle.setAttribute('aria-checked', String(enabled)); marketLinkToggle.classList.toggle('sg-form-active', enabled); marketLinkToggle.classList.toggle('sg-checkbox-field--inactive-selectable', !enabled); syncMarketLinkState(); updateFormState(); });";
|
||||
echo " marketOccurrenceInput?.addEventListener('change', () => { syncMarketDayOptions(); updateFormState(); });";
|
||||
echo " marketDayInput?.addEventListener('change', updateFormState);";
|
||||
echo " const submitOrder = async (orderData) => {";
|
||||
echo " let submissionSucceeded = false;";
|
||||
echo " const originalBtnText = confirmationSubmit.innerHTML;";
|
||||
@@ -1416,17 +1849,15 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " closeConfirmation();";
|
||||
echo " resetForm();";
|
||||
echo " toggleOverlay(false);";
|
||||
echo " } else if (errorEl) {";
|
||||
echo " errorEl.textContent = result.error || 'Unbekannter Fehler';";
|
||||
echo " errorEl.classList.remove('hidden');";
|
||||
echo " closeConfirmation();";
|
||||
echo " } else {";
|
||||
echo " const message = result.error || 'Unbekannter Fehler';";
|
||||
echo " if (errorEl) { errorEl.textContent = message; errorEl.classList.remove('hidden'); }";
|
||||
echo " if (confirmationError) { confirmationError.textContent = message; confirmationError.classList.remove('hidden'); }";
|
||||
echo " }";
|
||||
echo " } catch (error) {";
|
||||
echo " if (errorEl) {";
|
||||
echo " errorEl.textContent = 'Netzwerkfehler: ' + error.message;";
|
||||
echo " errorEl.classList.remove('hidden');";
|
||||
echo " }";
|
||||
echo " closeConfirmation();";
|
||||
echo " const message = 'Netzwerkfehler: ' + error.message;";
|
||||
echo " if (errorEl) { errorEl.textContent = message; errorEl.classList.remove('hidden'); }";
|
||||
echo " if (confirmationError) { confirmationError.textContent = message; confirmationError.classList.remove('hidden'); }";
|
||||
echo " } finally {";
|
||||
echo " confirmationSubmit.innerHTML = originalBtnText;";
|
||||
echo " confirmationSubmit.disabled = false;";
|
||||
@@ -1443,14 +1874,16 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " products: products,";
|
||||
echo " orderDate: orderDateInput ? orderDateInput.value : '',";
|
||||
echo " payments: Array.from(paymentRows.querySelectorAll('[data-otc-payment-row]')).map((row) => ({ paymentMethod: row.querySelector('[data-otc-payment-method]').value, amount: parseFloat(row.querySelector('[data-otc-payment-amount]').value || '0') || 0 })),";
|
||||
echo " billing: {";
|
||||
echo " billing: {";
|
||||
echo " firstName: getFieldValue(firstNameInput),";
|
||||
echo " lastName: getFieldValue(lastNameInput),";
|
||||
echo " street: getFieldValue(streetInput),";
|
||||
echo " houseNumber: getFieldValue(houseNumberInput),";
|
||||
echo " zip: getFieldValue(zipInput),";
|
||||
echo " city: getFieldValue(cityInput)";
|
||||
echo " }";
|
||||
echo " city: getFieldValue(cityInput)";
|
||||
echo " },";
|
||||
echo " marketOccurrenceId: isMarketLinkEnabled() ? (marketDayInput?.selectedOptions[0]?.dataset.marketOccurrenceId || null) : null,";
|
||||
echo " marketOccurrenceDayId: isMarketLinkEnabled() ? getFieldValue(marketDayInput) : null";
|
||||
echo " };";
|
||||
echo " openConfirmation(orderData);";
|
||||
echo " });";
|
||||
@@ -1458,6 +1891,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " if (event.target.closest('[data-otc-confirmation-cancel]')) { closeConfirmation(); return; }";
|
||||
echo " if (event.target.closest('[data-otc-confirmation-submit]') && pendingOrderData) { submitOrder(pendingOrderData); }";
|
||||
echo " });";
|
||||
echo " syncMarketLinkState();";
|
||||
echo " updateFormState();";
|
||||
echo "})();";
|
||||
echo "if (window.sgInitHelpIconOverlays) {";
|
||||
@@ -1466,8 +1900,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " outsideClickIgnoreSelectors: ['.sg-sandwich-menu-wrap'],";
|
||||
echo " });";
|
||||
echo "}";
|
||||
echo "renderMainHeading('ERP');";
|
||||
echo "renderLeftNavigation('ERP');";
|
||||
echo "(() => { const url = new URL(window.location.href); const requestedModule = url.searchParams.get('portal_module') || 'ERP'; const moduleButton = Array.from(document.querySelectorAll('.sg-portal-header__tabs .sg-tab-button')).find((button) => button.textContent.trim() === requestedModule) || document.querySelector('.sg-portal-header__tabs .sg-tab-button'); if (!moduleButton) { return; } const moduleName = moduleButton.textContent.trim(); document.querySelectorAll('.sg-portal-header__tabs .sg-tab-button').forEach((button) => { const active = button === moduleButton; button.setAttribute('aria-selected', String(active)); button.dataset.componentState = active ? 'active' : 'inactive'; }); renderMainHeading(moduleName); renderLeftNavigation(moduleName, url.searchParams.get('portal_section') || ''); })();";
|
||||
echo '</script>';
|
||||
echo '</body>';
|
||||
echo '</html>';
|
||||
|
||||
@@ -10,7 +10,7 @@ function throttle_webhook_channel(string $channel, int $minIntervalSeconds): voi
|
||||
$sanitizedChannel = preg_replace('/[^a-z0-9_-]+/i', '_', $channel) ?? 'default';
|
||||
$lockPath = sys_get_temp_dir() . '/erp_naurua_webhook_throttle_' . $sanitizedChannel . '.lock';
|
||||
|
||||
$handle = fopen($lockPath, 'c+');
|
||||
$handle = @fopen($lockPath, 'c+');
|
||||
if ($handle === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ require_once __DIR__ . '/../shared/db.php';
|
||||
require_once __DIR__ . '/../erp/lager/forecast.php';
|
||||
require_once __DIR__ . '/../erp/import-integration/shopify-webhook-worker.php';
|
||||
require_once __DIR__ . '/../erp/import-integration/shopify-delta-run.php';
|
||||
require_once __DIR__ . '/../market_management/service.php';
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
@@ -42,6 +43,7 @@ try {
|
||||
}
|
||||
},
|
||||
'erp.lager.stock_lot_forecast_refresh' => static fn (): array => run_lager_stock_lot_forecast($pdo),
|
||||
'market_management.occurrence_status_completion' => static fn (): array => market_management_auto_complete_occurrences($pdo),
|
||||
];
|
||||
$results = [];
|
||||
foreach ($jobs as $name => $job) {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../modules/shared/db.php';
|
||||
require_once __DIR__ . '/../../modules/shared/auth/service.php';
|
||||
require_once __DIR__ . '/../../modules/market_management/service.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
$env = expand_env_values(parse_env_file(__DIR__ . '/../../.env'));
|
||||
$pdo = connect_database($env);
|
||||
auth_bootstrap_session();
|
||||
|
||||
if (auth_current_user($pdo) === null) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['ok' => false, 'error' => 'Nicht autorisiert.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['ok' => false, 'error' => 'Methode nicht erlaubt.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$csrf = (string) ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
|
||||
if (!auth_validate_csrf_token($csrf)) {
|
||||
http_response_code(419);
|
||||
echo json_encode(['ok' => false, 'error' => 'Ungültiges Sicherheits-Token.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$payload = json_decode((string) file_get_contents('php://input'), true);
|
||||
if (!is_array($payload)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'Ungültige Anfrage.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$fields = is_array($payload['fields'] ?? null) ? $payload['fields'] : [];
|
||||
if (($fields['organizer_id'] ?? '') === '__new__') {
|
||||
$organizer = market_management_create_organizer(
|
||||
$pdo,
|
||||
(string) ($fields['organizer_name'] ?? ''),
|
||||
(string) ($fields['organizer_email'] ?? ''),
|
||||
(string) ($fields['organizer_phone'] ?? '')
|
||||
);
|
||||
$fields['organizer_id'] = $organizer['id'];
|
||||
}
|
||||
if (($payload['action'] ?? 'create') === 'delete') {
|
||||
$marketId = (int) ($payload['id'] ?? 0);
|
||||
if ($marketId < 1) {
|
||||
throw new InvalidArgumentException('id ist erforderlich.');
|
||||
}
|
||||
if (($payload['confirmation'] ?? '') !== 'DELETE') {
|
||||
throw new InvalidArgumentException('Die Löschbestätigung muss exakt DELETE lauten.');
|
||||
}
|
||||
$market = market_management_set_market_archive($pdo, $marketId, true);
|
||||
} elseif (($payload['action'] ?? 'create') === 'update') {
|
||||
$marketId = (int) ($payload['id'] ?? 0);
|
||||
if ($marketId < 1) {
|
||||
throw new InvalidArgumentException('id ist erforderlich.');
|
||||
}
|
||||
$market = market_management_update_market($pdo, $marketId, [
|
||||
'name' => $fields['name'] ?? null,
|
||||
'category_id' => $fields['category_id'] ?? null,
|
||||
'organizer_id' => $fields['organizer_id'] ?? null,
|
||||
'website_url' => $fields['website_url'] ?? null,
|
||||
]);
|
||||
} else {
|
||||
$market = market_management_create_market($pdo, [
|
||||
'name' => $fields['name'] ?? null,
|
||||
'category_id' => $fields['category_id'] ?? null,
|
||||
'organizer_id' => $fields['organizer_id'] ?? null,
|
||||
'website_url' => $fields['website_url'] ?? null,
|
||||
]);
|
||||
}
|
||||
echo json_encode(['ok' => true, 'market' => $market], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} catch (Throwable $error) {
|
||||
http_response_code(422);
|
||||
echo json_encode(['ok' => false, 'error' => $error->getMessage()], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../modules/shared/db.php';
|
||||
require_once __DIR__ . '/../../modules/market_management/mcp.php';
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
$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'] : [];
|
||||
$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,
|
||||
];
|
||||
} else {
|
||||
throw new InvalidArgumentException('Unbekannte JSON-RPC-Methode: ' . $method);
|
||||
}
|
||||
|
||||
echo json_encode(['jsonrpc' => '2.0', 'id' => $id, 'result' => $result], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code($e instanceof RuntimeException && str_contains($e->getMessage(), 'Token') ? 401 : 400);
|
||||
echo json_encode([
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => $request['id'] ?? null,
|
||||
'error' => ['code' => -32600, 'message' => $e->getMessage()],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?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 ($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);
|
||||
@@ -62,6 +62,7 @@
|
||||
--surface-menu-panel-portal: var(--color-light-grey);
|
||||
--surface-help-icon: var(--color-medium-grey);
|
||||
--surface-help-panel: var(--color-light-grey);
|
||||
--surface-tooltip: var(--color-light-grey);
|
||||
--surface-pulldown-panel: var(--color-light-grey);
|
||||
--surface-activatable-remove: var(--surface-control-default);
|
||||
--surface-slider-track: var(--color-medium-grey);
|
||||
@@ -89,6 +90,8 @@
|
||||
--layout-mode-toggle-local-width-factor: 3;
|
||||
--layout-mode-toggle-width: calc(var(--interaction-height) * 3.5);
|
||||
--layout-help-panel-width: calc(var(--interaction-height) * 8);
|
||||
--layout-tooltip-width: calc(var(--interaction-height) * 8);
|
||||
--radius-tooltip: var(--radius-interaction);
|
||||
--text-control-default: var(--color-font-dark);
|
||||
--text-control-disabled: var(--color-dark-grey);
|
||||
--text-button-process: var(--color-font-light);
|
||||
@@ -98,6 +101,7 @@
|
||||
--text-menu-link-portal: var(--color-font-dark);
|
||||
--text-help-icon: var(--color-font-dark);
|
||||
--text-help-panel: var(--color-font-dark);
|
||||
--text-tooltip: var(--color-font-dark);
|
||||
--text-activatable-remove: var(--text-control-default);
|
||||
--text-hyperlink: var(--color-font-hyperlink);
|
||||
--icon-pulldown-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M2 2l4 4 4-4' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='square' stroke-linejoin='miter'/%3E%3C/svg%3E");
|
||||
@@ -1760,6 +1764,95 @@ section + section {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel {
|
||||
position: absolute;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
display: block;
|
||||
width: var(--layout-tooltip-width);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: anywhere;
|
||||
padding: var(--spacing-large);
|
||||
border-radius: var(--radius-tooltip);
|
||||
color: var(--text-tooltip);
|
||||
background: var(--surface-tooltip);
|
||||
box-shadow: var(--shadow-overlay);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel {
|
||||
top: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel {
|
||||
bottom: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
right: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
left: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel::before {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel::before {
|
||||
top: calc(var(--spacing-small) * -1);
|
||||
left: 50%;
|
||||
border-width: 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent var(--surface-tooltip);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel::before {
|
||||
bottom: calc(var(--spacing-small) * -1);
|
||||
left: 50%;
|
||||
border-width: var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: var(--surface-tooltip) transparent transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel::before {
|
||||
top: 50%;
|
||||
right: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent transparent var(--surface-tooltip);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel::before {
|
||||
top: 50%;
|
||||
left: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-pulldown-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + var(--spacing-small));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"styleguideVersion": "2026.05.18.1",
|
||||
"styleguideCommit": "71734d6",
|
||||
"syncedAtUtc": "2026-09-08T09:49:27Z",
|
||||
"styleguideCommit": "944d791",
|
||||
"syncedAtUtc": "2026-09-21T12:42:55Z",
|
||||
"sourceRepo": "/Users/mathias/Documents/Dokumente Chouchou/Codebases/Styleguide",
|
||||
"mirroredDocsPath": "docs/styleguide"
|
||||
}
|
||||
@@ -62,6 +62,7 @@
|
||||
--surface-menu-panel-portal: var(--color-light-grey);
|
||||
--surface-help-icon: var(--color-medium-grey);
|
||||
--surface-help-panel: var(--color-light-grey);
|
||||
--surface-tooltip: var(--color-light-grey);
|
||||
--surface-pulldown-panel: var(--color-light-grey);
|
||||
--surface-activatable-remove: var(--surface-control-default);
|
||||
--surface-slider-track: var(--color-medium-grey);
|
||||
@@ -89,6 +90,8 @@
|
||||
--layout-mode-toggle-local-width-factor: 3;
|
||||
--layout-mode-toggle-width: calc(var(--interaction-height) * 3.5);
|
||||
--layout-help-panel-width: calc(var(--interaction-height) * 8);
|
||||
--layout-tooltip-width: calc(var(--interaction-height) * 8);
|
||||
--radius-tooltip: var(--radius-interaction);
|
||||
--text-control-default: var(--color-font-dark);
|
||||
--text-control-disabled: var(--color-dark-grey);
|
||||
--text-button-process: var(--color-font-light);
|
||||
@@ -98,6 +101,7 @@
|
||||
--text-menu-link-portal: var(--color-font-dark);
|
||||
--text-help-icon: var(--color-font-dark);
|
||||
--text-help-panel: var(--color-font-dark);
|
||||
--text-tooltip: var(--color-font-dark);
|
||||
--text-activatable-remove: var(--text-control-default);
|
||||
--text-hyperlink: var(--color-font-hyperlink);
|
||||
--icon-pulldown-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M2 2l4 4 4-4' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='square' stroke-linejoin='miter'/%3E%3C/svg%3E");
|
||||
@@ -1760,6 +1764,95 @@ section + section {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.sg-tooltip-panel {
|
||||
position: absolute;
|
||||
z-index: var(--layer-pulldown-panel);
|
||||
display: block;
|
||||
width: var(--layout-tooltip-width);
|
||||
max-width: calc(100vw - (2 * var(--spacing-large)));
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: anywhere;
|
||||
padding: var(--spacing-large);
|
||||
border-radius: var(--radius-tooltip);
|
||||
color: var(--text-tooltip);
|
||||
background: var(--surface-tooltip);
|
||||
box-shadow: var(--shadow-overlay);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel {
|
||||
top: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel {
|
||||
bottom: calc(100% + var(--spacing-small));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
right: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel {
|
||||
top: 50%;
|
||||
left: calc(100% + var(--spacing-small));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-panel::before {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="bottom"] .sg-tooltip-panel::before {
|
||||
top: calc(var(--spacing-small) * -1);
|
||||
left: 50%;
|
||||
border-width: 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent var(--surface-tooltip);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="top"] .sg-tooltip-panel::before {
|
||||
bottom: calc(var(--spacing-small) * -1);
|
||||
left: 50%;
|
||||
border-width: var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: var(--surface-tooltip) transparent transparent;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="left"] .sg-tooltip-panel::before {
|
||||
top: 50%;
|
||||
right: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
|
||||
border-color: transparent transparent transparent var(--surface-tooltip);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-tooltip-wrap[data-tooltip-placement="right"] .sg-tooltip-panel::before {
|
||||
top: 50%;
|
||||
left: calc(var(--spacing-small) * -1);
|
||||
border-width: var(--spacing-small) var(--spacing-small) var(--spacing-small) 0;
|
||||
border-color: transparent var(--surface-tooltip) transparent transparent;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sg-pulldown-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + var(--spacing-small));
|
||||
|
||||
@@ -39,6 +39,75 @@ 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-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;
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -246,8 +315,12 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sg-large-table--markets .sg-large-table__row {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sg-large-table--lager[data-lager-table-key="products"] .sg-large-table__row {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
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 +343,80 @@ 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-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;
|
||||
|
||||
@@ -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);
|
||||
+37
-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,45 @@ 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;
|
||||
$marketDefinitionsTable = [
|
||||
'rows' => market_management_search_markets($pdo, [
|
||||
'include_archived' => false,
|
||||
'sort' => 'name',
|
||||
'sort_direction' => 'ASC',
|
||||
'limit' => 100,
|
||||
'offset' => 0,
|
||||
]),
|
||||
'categories' => market_management_list_categories($pdo),
|
||||
'organizers' => market_management_list_organizers($pdo),
|
||||
];
|
||||
if ((string) ($_GET['bestellungen_fragment'] ?? '') === '1') {
|
||||
echo auth_render_bestellungen_content($bestellungenTable);
|
||||
exit;
|
||||
}
|
||||
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,129 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const cardSelector = '[data-shared-editable-action-card]';
|
||||
|
||||
const getCard = (element) => element.closest(cardSelector);
|
||||
|
||||
const getDirectDeleteElement = (card, selector) => Array.from(card?.children || [])
|
||||
.find((child) => child.matches(selector)) || null;
|
||||
|
||||
const setEditMode = (card, enabled) => {
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
const readView = card.querySelector('[data-shared-editable-card-read]');
|
||||
const editView = card.querySelector('[data-shared-editable-card-form]');
|
||||
const editButtons = card.querySelectorAll('[data-shared-editable-card-edit]');
|
||||
|
||||
if (readView) {
|
||||
readView.hidden = enabled;
|
||||
}
|
||||
if (editView) {
|
||||
editView.hidden = !enabled;
|
||||
}
|
||||
editButtons.forEach((editButton) => {
|
||||
editButton.disabled = enabled;
|
||||
editButton.setAttribute('aria-disabled', String(enabled));
|
||||
editButton.classList.toggle('sg-button--inactive', enabled);
|
||||
});
|
||||
};
|
||||
|
||||
const resetDeleteConfirmation = (card) => {
|
||||
const 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 edit = event.target.closest('[data-shared-editable-card-edit]');
|
||||
if (edit && root.contains(edit)) {
|
||||
setEditMode(getCard(edit), 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);
|
||||
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));
|
||||
}
|
||||
});
|
||||
|
||||
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 };
|
||||
}());
|
||||
Reference in new issue
Block a user