Gemeinderegister für Marktdurchführungen ergänzen

This commit is contained in:
gitea_admin committed 2026-09-20 11:48:00 +02:00
1 parent 273fb05833
commit 2d3c9ce4ab
8 files changed
+177 -18

No files matched your search

@@ -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;
@@ -14,6 +14,7 @@ Status: Verbindliches DB-Ownership-Konzept
| `market_occurrence_day` | `market_management` | Marktmanagement-Service | | `market_occurrence_day` | `market_management` | Marktmanagement-Service |
| `market_occurrence_cost_item` | `market_management` | Marktmanagement-Service | | `market_occurrence_cost_item` | `market_management` | Marktmanagement-Service |
| `market_municipality_reference` | `market_management` | Referenzdatenprozess | | `market_municipality_reference` | `market_management` | Referenzdatenprozess |
| `market_municipality` | `market_management` | Marktmanagement-Service |
| `market_mcp_idempotency` | `market_management` | MCP-Submodul | | `market_mcp_idempotency` | `market_management` | MCP-Submodul |
## 2. Fremde Datenreferenzen ## 2. Fremde Datenreferenzen
@@ -44,6 +45,11 @@ Umsatz- und Artikelmengenaggregation.
Der Gemeindereferenzbestand verwendet die BFS-Gemeindenummer als stabile Der Gemeindereferenzbestand verwendet die BFS-Gemeindenummer als stabile
fachliche Referenz und speichert den Datenstand zur Nachvollziehbarkeit. 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 Die Kategorie-/Marktartliste gehört zum Marktmanagement und wird durch Fabienne
gepflegt. Hermes darf sie nur lesen. gepflegt. Hermes darf sie nur lesen.
@@ -120,7 +120,10 @@ Hermes darf Kategorien lesen und bei einem Markt referenzieren, aber keine
Kategorien anlegen oder ändern. Kategorien anlegen oder ändern.
- `market_management.list_reference_data` - `market_management.list_reference_data`
- liefert Kategorien, Kantone und aktive Gemeinden - liefert Kategorien, Kantone und aktive Gemeinden aus dem Markt-Gemeinderegister
- `market_management.create_municipality`
- erforderlich: `name`
- legt eine lokale Gemeinde im Markt-Gemeinderegister an
- `market_management.get_overview` - `market_management.get_overview`
- liefert serverseitig gefilterte Markt- und Marktdurchführungslisten - liefert serverseitig gefilterte Markt- und Marktdurchführungslisten
@@ -136,8 +139,9 @@ Kategorien anlegen oder ändern.
berechnete Tages- und Gesamterlöse berechnete Tages- und Gesamterlöse
- `market_management.update_occurrence` - `market_management.update_occurrence`
- erforderlich: `occurrence_id` - erforderlich: `occurrence_id`
- änderbar: sämtliche GUI-Marktdurchführungsfelder einschließlich Veranstalter, - änderbar: sämtliche editierbaren GUI-Marktdurchführungsfelder einschließlich
Kontaktperson, Kontakt-E-Mail und Kontakt-Telefon Gemeinde, Kontaktperson, Kontakt-E-Mail und Kontakt-Telefon
- der vom Markt geerbte Veranstalter ist nicht editierbar
- `market_management.archive_occurrence` - `market_management.archive_occurrence`
- erforderlich: `occurrence_id` - erforderlich: `occurrence_id`
- `market_management.reactivate_occurrence` - `market_management.reactivate_occurrence`
@@ -14,6 +14,7 @@ Tabellen:
- `market_occurrence_day` - `market_occurrence_day`
- `market_occurrence_cost_item` - `market_occurrence_cost_item`
- `market_municipality_reference` - `market_municipality_reference`
- `market_municipality`
- `market_mcp_idempotency` - `market_mcp_idempotency`
Bestehende Integrationsreferenz: Bestehende Integrationsreferenz:
@@ -176,9 +177,24 @@ Normativ:
- Gemeinden werden über BFS-Gemeindenummern referenziert - Gemeinden werden über BFS-Gemeindenummern referenziert
- Gemeindename und Kantonszuordnung stammen aus dem Referenzbestand - Gemeindename und Kantonszuordnung stammen aus dem Referenzbestand
- nur gültige Gemeinden stehen für neue Eingaben zur Auswahl - 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 - der Datenstand wird versioniert gespeichert
### 3.8 `market_mcp_idempotency` ### 3.9 `market_mcp_idempotency`
Technischer Idempotenzbestand für wiederholte Hermes-Schreibaufrufe. Technischer Idempotenzbestand für wiederholte Hermes-Schreibaufrufe.
+6
View File
@@ -81,6 +81,7 @@ function market_management_mcp_tools(): array
['name' => 'market_management.list_market_organizers', 'description' => 'Aktive Veranstalter 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.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.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.get_overview', 'description' => 'Marktübersicht mit serverseitigen Filtern lesen', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]],
['name' => 'market_management.create_occurrence', 'description' => 'Marktdurchführung mit mindestens einem Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id', 'days'], 'properties' => ['market_definition_id' => ['type' => 'integer'], 'status' => ['type' => 'string'], 'days' => ['type' => 'array', 'minItems' => 1, 'items' => ['type' => 'object', 'required' => ['date'], 'properties' => $dayProperties]] + $occurrenceProperties]]], ['name' => 'market_management.create_occurrence', 'description' => 'Marktdurchführung mit mindestens einem Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id', 'days'], '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.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']]]],
@@ -101,6 +102,7 @@ function market_management_mcp_is_write_tool(string $tool): bool
return in_array($tool, [ return in_array($tool, [
'market_management.create_market', 'market_management.update_market', 'market_management.create_market', 'market_management.update_market',
'market_management.create_market_organizer', 'market_management.create_market_organizer',
'market_management.create_municipality',
'market_management.archive_market', 'market_management.reactivate_market', 'market_management.archive_market', 'market_management.reactivate_market',
'market_management.create_occurrence', 'market_management.update_occurrence', 'market_management.create_occurrence', 'market_management.update_occurrence',
'market_management.archive_occurrence', 'market_management.reactivate_occurrence', 'market_management.archive_occurrence', 'market_management.reactivate_occurrence',
@@ -276,6 +278,10 @@ function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): a
$arguments['phone'] ?? null $arguments['phone'] ?? null
), ),
'market_management.list_reference_data' => market_management_list_reference_data($pdo, $arguments), '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' => [ 'market_management.get_overview' => [
'markets' => market_management_search_markets($pdo, $arguments), 'markets' => market_management_search_markets($pdo, $arguments),
'occurrences' => market_management_search_occurrences($pdo, $arguments), 'occurrences' => market_management_search_occurrences($pdo, $arguments),
+95 -14
View File
@@ -55,6 +55,40 @@ function market_management_create_organizer(PDO $pdo, string $name, ?string $ema
return (array) $stmt->fetch(); return (array) $stmt->fetch();
} }
function market_management_list_municipalities(PDO $pdo): array
{
return $pdo->query(
"SELECT id, name, canton_code, bfs_municipality_number, data_version, source
FROM market_municipality
WHERE is_active = TRUE
ORDER BY name ASC, id ASC"
)->fetchAll() ?: [];
}
function market_management_create_municipality(PDO $pdo, string $name): array
{
$name = trim($name);
if ($name === '') {
throw new InvalidArgumentException('Gemeindename fehlt.');
}
$stmt = $pdo->prepare(
"SELECT 1 FROM market_municipality WHERE is_active = TRUE AND LOWER(TRIM(name)) = LOWER(TRIM(:name))"
);
$stmt->execute([':name' => $name]);
if ($stmt->fetchColumn() !== false) {
throw new InvalidArgumentException('Diese Gemeinde ist bereits vorhanden.');
}
$stmt = $pdo->prepare(
"INSERT INTO market_municipality (name, source)
VALUES (:name, 'local')
RETURNING id, name, canton_code, bfs_municipality_number, data_version, source"
);
$stmt->execute([':name' => $name]);
return (array) $stmt->fetch();
}
function market_management_require_positive_id(mixed $value, string $field): int function market_management_require_positive_id(mixed $value, string $field): int
{ {
if (!is_int($value) && !ctype_digit((string) $value)) { if (!is_int($value) && !ctype_digit((string) $value)) {
@@ -501,10 +535,7 @@ function market_management_list_active_occurrence_options(PDO $pdo): array
function market_management_list_reference_data(PDO $pdo, array $input): array function market_management_list_reference_data(PDO $pdo, array $input): array
{ {
$result = ['categories' => market_management_list_categories($pdo)]; $result = ['categories' => market_management_list_categories($pdo)];
$municipalities = $pdo->query( $municipalities = market_management_list_municipalities($pdo);
'SELECT id, bfs_municipality_number, name, canton_code, data_version
FROM market_municipality_reference WHERE is_active = TRUE ORDER BY name ASC, id ASC'
)->fetchAll();
$result['municipalities'] = $municipalities; $result['municipalities'] = $municipalities;
$result['cantons'] = array_values(array_map( $result['cantons'] = array_values(array_map(
static fn (array $row): array => ['code' => (string) $row['canton_code']], static fn (array $row): array => ['code' => (string) $row['canton_code']],
@@ -622,6 +653,14 @@ function market_management_write_fields(array $input, array $definitions): array
function market_management_create_occurrence(PDO $pdo, array $input): array function market_management_create_occurrence(PDO $pdo, array $input): array
{ {
$newMunicipalityName = null;
if (($input['municipality_reference_id'] ?? null) === '__new__') {
$newMunicipalityName = trim((string) ($input['municipality_name'] ?? ''));
if ($newMunicipalityName === '') {
throw new InvalidArgumentException('Gemeindename fehlt.');
}
unset($input['municipality_reference_id'], $input['municipality_name']);
}
$marketId = market_management_require_positive_id($input['market_definition_id'] ?? null, 'market_definition_id'); $marketId = market_management_require_positive_id($input['market_definition_id'] ?? null, 'market_definition_id');
if (market_management_get_market($pdo, $marketId, true) === null) { if (market_management_get_market($pdo, $marketId, true) === null) {
throw new InvalidArgumentException('market_definition_id verweist nicht auf einen Markt.'); throw new InvalidArgumentException('market_definition_id verweist nicht auf einen Markt.');
@@ -658,6 +697,13 @@ function market_management_create_occurrence(PDO $pdo, array $input): array
$pdo->beginTransaction(); $pdo->beginTransaction();
} }
try { try {
if ($newMunicipalityName !== null) {
$municipality = market_management_create_municipality($pdo, $newMunicipalityName);
$fields[] = 'municipality_reference_id = :municipality_reference_id';
$params[':municipality_reference_id'] = (int) $municipality['id'];
$columns = array_map(static fn (string $field): string => explode(' = ', $field, 2)[0], $fields);
$values = array_map(static fn (string $field): string => explode(' = ', $field, 2)[1], $fields);
}
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
'INSERT INTO market_occurrence (' . implode(', ', $columns) . ') 'INSERT INTO market_occurrence (' . implode(', ', $columns) . ')
VALUES (' . implode(', ', $values) . ') VALUES (' . implode(', ', $values) . ')
@@ -702,6 +748,14 @@ function market_management_get_occurrence(PDO $pdo, int $occurrenceId, bool $inc
if (!is_array($occurrence)) { if (!is_array($occurrence)) {
return null; return null;
} }
$municipality = $pdo->prepare(
'SELECT id, name, canton_code, bfs_municipality_number, data_version, source
FROM market_municipality WHERE id = :id AND is_active = TRUE'
);
$municipality->execute([':id' => $occurrence['municipality_reference_id'] ?? null]);
$municipality = $municipality->fetch();
$occurrence['municipality'] = is_array($municipality) ? $municipality : null;
$occurrence['municipality_name'] = is_array($municipality) ? (string) $municipality['name'] : null;
$days = $pdo->prepare( $days = $pdo->prepare(
'SELECT * FROM market_occurrence_day 'SELECT * FROM market_occurrence_day
WHERE occurrence_id = :occurrence_id ORDER BY date ASC, id ASC' WHERE occurrence_id = :occurrence_id ORDER BY date ASC, id ASC'
@@ -837,6 +891,14 @@ function market_management_get_occurrence(PDO $pdo, int $occurrenceId, bool $inc
function market_management_update_occurrence(PDO $pdo, int $occurrenceId, array $input): array function market_management_update_occurrence(PDO $pdo, int $occurrenceId, array $input): array
{ {
$newMunicipalityName = null;
if (($input['municipality_reference_id'] ?? null) === '__new__') {
$newMunicipalityName = trim((string) ($input['municipality_name'] ?? ''));
if ($newMunicipalityName === '') {
throw new InvalidArgumentException('Gemeindename fehlt.');
}
unset($input['municipality_reference_id'], $input['municipality_name']);
}
$definitions = [ $definitions = [
'status' => 'string', 'place' => 'string', 'address' => 'string', 'municipality_reference_id' => 'nullable_id', 'status' => 'string', 'place' => 'string', 'address' => 'string', 'municipality_reference_id' => 'nullable_id',
'notes' => 'string', 'registration_deadline' => 'string', 'request_or_registration_date' => 'string', 'notes' => 'string', 'registration_deadline' => 'string', 'request_or_registration_date' => 'string',
@@ -858,18 +920,37 @@ function market_management_update_occurrence(PDO $pdo, int $occurrenceId, array
if (isset($params[':payment_status']) && !in_array($params[':payment_status'], ['paid', 'unpaid'], true)) { if (isset($params[':payment_status']) && !in_array($params[':payment_status'], ['paid', 'unpaid'], true)) {
throw new InvalidArgumentException('Ungültiger Zahlungsstatus.'); throw new InvalidArgumentException('Ungültiger Zahlungsstatus.');
} }
if ($fields === []) { if ($fields === [] && $newMunicipalityName === null) {
throw new InvalidArgumentException('Mindestens ein änderbares Feld ist erforderlich.'); throw new InvalidArgumentException('Mindestens ein änderbares Feld ist erforderlich.');
} }
$fields[] = 'updated_at = NOW()'; $startedTransaction = $newMunicipalityName !== null && !$pdo->inTransaction();
$params[':id'] = $occurrenceId; if ($startedTransaction) {
$stmt = $pdo->prepare( $pdo->beginTransaction();
'UPDATE market_occurrence SET ' . implode(', ', $fields) . ' WHERE id = :id RETURNING *' }
); try {
$stmt->execute($params); if ($newMunicipalityName !== null) {
$occurrence = $stmt->fetch(); $municipality = market_management_create_municipality($pdo, $newMunicipalityName);
if (!is_array($occurrence)) { $fields[] = 'municipality_reference_id = :municipality_reference_id';
throw new RuntimeException('Marktdurchführung nicht gefunden.'); $params[':municipality_reference_id'] = (int) $municipality['id'];
}
$fields[] = 'updated_at = NOW()';
$params[':id'] = $occurrenceId;
$stmt = $pdo->prepare(
'UPDATE market_occurrence SET ' . implode(', ', $fields) . ' WHERE id = :id RETURNING *'
);
$stmt->execute($params);
$occurrence = $stmt->fetch();
if (!is_array($occurrence)) {
throw new RuntimeException('Marktdurchführung nicht gefunden.');
}
if ($startedTransaction) {
$pdo->commit();
}
} catch (Throwable $error) {
if ($startedTransaction && $pdo->inTransaction()) {
$pdo->rollBack();
}
throw $error;
} }
return $occurrence; return $occurrence;
} }
+2
View File
@@ -1143,6 +1143,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
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 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 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 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 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 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 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 marketEditButton = () => '<button class=\"sg-interaction-element sg-button sg-button--active\" type=\"button\" data-shared-editable-card-edit>Bearbeiten</button>';";
@@ -1180,6 +1181,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
echo " const deleteMarketOccurrence = 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-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 deleteMarketOccurrence = 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-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 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 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 " if (!marketsBindingsInstalled) {"; echo " if (!marketsBindingsInstalled) {";
echo " marketsBindingsInstalled = true;"; 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 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); } } });";
+4
View File
@@ -358,6 +358,10 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
display: none; display: none;
} }
.sg-card-list-page-drawer [data-market-new-municipality][hidden] {
display: none;
}
[data-shared-editable-card-read][hidden], [data-shared-editable-card-read][hidden],
[data-shared-editable-card-form][hidden] { [data-shared-editable-card-form][hidden] {
display: none; display: none;