diff --git a/db/README.md b/db/README.md
index b828dc6..061edc3 100644
--- a/db/README.md
+++ b/db/README.md
@@ -20,6 +20,8 @@
16. `db/migrations/0020_include_stock_adjustments_in_balance.sql`
17. `db/migrations/0021_stock_lot_activation_selection.sql`
18. `db/migrations/0022_backfill_stock_lot_activation_selection.sql`
+19. `db/migrations/0023_market_management.sql` bis `db/migrations/0034_market_occurrence_checklist.sql`
+20. `db/migrations/0035_market_occurrence_checklist_chapters.sql`
## Beispielausfuehrung
diff --git a/db/migrations/0035_market_occurrence_checklist_chapters.sql b/db/migrations/0035_market_occurrence_checklist_chapters.sql
new file mode 100644
index 0000000..001a7fb
--- /dev/null
+++ b/db/migrations/0035_market_occurrence_checklist_chapters.sql
@@ -0,0 +1,53 @@
+BEGIN;
+
+ALTER TABLE market_occurrence_checklist_item
+ ADD COLUMN chapter_key TEXT;
+
+DELETE FROM market_occurrence_checklist_item;
+UPDATE market_occurrence_checklist
+SET comment = NULL,
+ updated_at = NOW();
+
+ALTER TABLE market_occurrence_checklist_item
+ ALTER COLUMN chapter_key SET NOT NULL,
+ ADD CONSTRAINT chk_market_occurrence_checklist_item_chapter
+ CHECK (chapter_key IN ('products_sales', 'decoration', 'stand_equipment', 'technology'));
+
+INSERT INTO market_occurrence_checklist_item
+ (occurrence_id, chapter_key, position, label, is_completed)
+SELECT checklist.occurrence_id,
+ defaults.chapter_key,
+ defaults.position,
+ defaults.label,
+ FALSE
+FROM market_occurrence_checklist checklist
+CROSS JOIN (
+ VALUES
+ ('products_sales', 1, 'Fläschchen'),
+ ('products_sales', 2, 'Säckchen'),
+ ('products_sales', 3, 'Dunkle Kleber'),
+ ('products_sales', 4, 'Naurua-Kleber'),
+ ('products_sales', 5, 'Anwendungsbeilage'),
+ ('products_sales', 6, 'Flyer Naurua'),
+ ('products_sales', 7, 'Flyer Vitalpilze'),
+ ('products_sales', 8, 'Vitalpilzbuch'),
+ ('products_sales', 9, 'SumUp-Terminal'),
+ ('products_sales', 10, 'Bargeld'),
+ ('decoration', 1, 'Dekoration'),
+ ('decoration', 2, 'Pilze'),
+ ('decoration', 3, 'Blumenvase'),
+ ('decoration', 4, 'Blume'),
+ ('stand_equipment', 1, 'Tisch'),
+ ('stand_equipment', 2, 'Tischtücher'),
+ ('stand_equipment', 3, 'Beleuchtung'),
+ ('stand_equipment', 4, 'Holzklötze'),
+ ('stand_equipment', 5, 'Wagen'),
+ ('stand_equipment', 6, 'Sonnenschirm'),
+ ('stand_equipment', 7, 'Parkkarte/-Marktinformationen'),
+ ('technology', 1, 'Batterie-/Powerbank fürs Handy')
+) AS defaults(chapter_key, position, label);
+
+CREATE INDEX idx_market_occurrence_checklist_item_chapter
+ ON market_occurrence_checklist_item(occurrence_id, chapter_key, position, id);
+
+COMMIT;
diff --git a/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md b/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md
index 4cf58d6..eca0b1d 100644
--- a/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md
+++ b/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md
@@ -179,13 +179,16 @@ Kategorien anlegen oder ändern.
berechnete Tages- und Gesamterlöse
- `market_management.get_occurrence_checklist`
- erforderlich: `occurrence_id`
- - liefert die 30 initialen beziehungsweise individuell gepflegten Punkte
- und den Checklistenkommentar
+ - liefert die vier festen Kapitel, die Punkte mit `chapter_key` und den
+ Checklistenkommentar
- `market_management.update_occurrence_checklist`
- erforderlich: `occurrence_id`, `items`
- ersetzt die vollständige Checkliste atomar
- - `items` enthält `label` und `is_completed`; `comment` ist optional und nullable
- - Hermes darf Punkte ergänzen, entfernen, umbenennen, abhaken und zurücksetzen
+ - `items` enthält `chapter_key`, `label` und `is_completed`; `comment` ist optional und nullable
+ - feste Kapitel: `products_sales` (Produkte & Verkauf), `decoration` (Dekoration),
+ `stand_equipment` (Stand & Ausstattung), `technology` (Technik)
+ - Kapitel sind nicht editierbar; Hermes darf Punkte ergänzen, entfernen,
+ umbenennen, abhaken und zurücksetzen
- `market_management.update_occurrence`
- erforderlich: `occurrence_id`
- änderbar: sämtliche editierbaren GUI-Marktdurchführungsfelder einschließlich
diff --git a/docs/modules/market_management/konzepte/market_management_technisches_konzept.md b/docs/modules/market_management/konzepte/market_management_technisches_konzept.md
index 1a51a83..fb13d3a 100644
--- a/docs/modules/market_management/konzepte/market_management_technisches_konzept.md
+++ b/docs/modules/market_management/konzepte/market_management_technisches_konzept.md
@@ -36,6 +36,12 @@ Prozessbereiche:
- Marktdurchführungs-Checkliste verwalten
- Referenzdaten verwalten
+Die Checkliste gehört zur Marktdurchführung, nicht zum einzelnen Markttag.
+Sie besitzt die vier festen Kapitel `Produkte & Verkauf`, `Dekoration`,
+`Stand & Ausstattung` und `Technik`. Kapitel und Kapitelreihenfolge sind
+fest; Punkte innerhalb eines Kapitels bleiben für Benutzer und Hermes
+editierbar. Der Kommentar gehört zur gesamten Checkliste.
+
## 2. Zweck und Scope
Dieses Dokument beschreibt das normative technische Zielbild für die Umsetzung
diff --git a/modules/market_management/mcp.php b/modules/market_management/mcp.php
index 30cfce5..1f78720 100644
--- a/modules/market_management/mcp.php
+++ b/modules/market_management/mcp.php
@@ -88,7 +88,7 @@ function market_management_mcp_tools(): array
['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_checklist', 'description' => 'Checkliste einer Marktdurchführung lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]],
- ['name' => 'market_management.update_occurrence_checklist', 'description' => 'Checkliste einer Marktdurchführung vollständig ersetzen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'items'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'comment' => ['type' => ['string', 'null']], 'items' => ['type' => 'array', 'items' => ['type' => 'object', 'required' => ['label', 'is_completed'], 'properties' => ['label' => ['type' => 'string'], 'is_completed' => ['type' => 'boolean']]]]]]],
+ ['name' => 'market_management.update_occurrence_checklist', 'description' => 'Checkliste einer Marktdurchführung vollständig ersetzen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'items'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'comment' => ['type' => ['string', 'null']], 'items' => ['type' => 'array', 'items' => ['type' => 'object', 'required' => ['chapter_key', 'label', 'is_completed'], 'properties' => ['chapter_key' => ['type' => 'string', 'enum' => ['products_sales', 'decoration', 'stand_equipment', 'technology']], 'label' => ['type' => 'string'], 'is_completed' => ['type' => 'boolean']]]]]]],
['name' => 'market_management.update_occurrence', 'description' => 'Marktdurchführung ändern', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], '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']]]],
diff --git a/modules/market_management/occurrence_checklist.php b/modules/market_management/occurrence_checklist.php
index 2bdb47c..9361d29 100644
--- a/modules/market_management/occurrence_checklist.php
+++ b/modules/market_management/occurrence_checklist.php
@@ -1,13 +1,39 @@
'products_sales',
+ 'label' => 'Produkte & Verkauf',
+ 'items' => ['Fläschchen', 'Säckchen', 'Dunkle Kleber', 'Naurua-Kleber', 'Anwendungsbeilage', 'Flyer Naurua', 'Flyer Vitalpilze', 'Vitalpilzbuch', 'SumUp-Terminal', 'Bargeld'],
+ ],
+ [
+ 'key' => 'decoration',
+ 'label' => 'Dekoration',
+ 'items' => ['Dekoration', 'Pilze', 'Blumenvase', 'Blume'],
+ ],
+ [
+ 'key' => 'stand_equipment',
+ 'label' => 'Stand & Ausstattung',
+ 'items' => ['Tisch', 'Tischtücher', 'Beleuchtung', 'Holzklötze', 'Wagen', 'Sonnenschirm', 'Parkkarte/-Marktinformationen'],
+ ],
+ [
+ 'key' => 'technology',
+ 'label' => 'Technik',
+ 'items' => ['Batterie-/Powerbank fürs Handy'],
+ ],
+];
function market_management_checklist_default_items(): array
{
$items = [];
- for ($index = 1; $index <= MARKET_OCCURRENCE_CHECKLIST_DEFAULT_ITEMS; $index++) {
- $items[] = 'Position ' . $index;
+ foreach (MARKET_OCCURRENCE_CHECKLIST_CHAPTERS as $chapter) {
+ foreach ($chapter['items'] as $label) {
+ $items[] = [
+ 'chapter_key' => $chapter['key'],
+ 'label' => $label,
+ ];
+ }
}
return $items;
}
@@ -38,17 +64,20 @@ function market_management_checklist_initialize(PDO $pdo, int $occurrenceId): vo
return;
}
- $item = $pdo->prepare(
+ $insert = $pdo->prepare(
'INSERT INTO market_occurrence_checklist_item
- (occurrence_id, position, label, is_completed)
- VALUES (:occurrence_id, :position, :label, :is_completed)'
+ (occurrence_id, chapter_key, position, label, is_completed)
+ VALUES (:occurrence_id, :chapter_key, :position, :label, :is_completed)'
);
- foreach (market_management_checklist_default_items() as $position => $label) {
- $item->bindValue(':occurrence_id', $occurrenceId, PDO::PARAM_INT);
- $item->bindValue(':position', $position + 1, PDO::PARAM_INT);
- $item->bindValue(':label', $label, PDO::PARAM_STR);
- $item->bindValue(':is_completed', false, PDO::PARAM_BOOL);
- $item->execute();
+ $chapterPositions = [];
+ foreach (market_management_checklist_default_items() as $item) {
+ $insert->bindValue(':occurrence_id', $occurrenceId, PDO::PARAM_INT);
+ $insert->bindValue(':chapter_key', $item['chapter_key'], PDO::PARAM_STR);
+ $chapterPositions[$item['chapter_key']] = ($chapterPositions[$item['chapter_key']] ?? 0) + 1;
+ $insert->bindValue(':position', $chapterPositions[$item['chapter_key']], PDO::PARAM_INT);
+ $insert->bindValue(':label', $item['label'], PDO::PARAM_STR);
+ $insert->bindValue(':is_completed', false, PDO::PARAM_BOOL);
+ $insert->execute();
}
}
@@ -66,13 +95,17 @@ function market_management_get_occurrence_checklist(PDO $pdo, int $occurrenceId,
throw new RuntimeException('Checkliste der Marktdurchführung fehlt.');
}
$items = $pdo->prepare(
- 'SELECT id, occurrence_id, position, label, is_completed, created_at, updated_at
+ 'SELECT id, occurrence_id, chapter_key, position, label, is_completed, created_at, updated_at
FROM market_occurrence_checklist_item
WHERE occurrence_id = :occurrence_id
- ORDER BY position ASC, id ASC'
+ ORDER BY id ASC'
);
$items->execute([':occurrence_id' => $occurrenceId]);
$checklist['items'] = $items->fetchAll();
+ $checklist['chapters'] = array_map(
+ static fn (array $chapter): array => ['key' => $chapter['key'], 'label' => $chapter['label']],
+ MARKET_OCCURRENCE_CHECKLIST_CHAPTERS
+ );
return $checklist;
}
@@ -105,13 +138,19 @@ function market_management_update_occurrence_checklist(PDO $pdo, int $occurrence
$delete->execute([':occurrence_id' => $occurrenceId]);
$insert = $pdo->prepare(
'INSERT INTO market_occurrence_checklist_item
- (occurrence_id, position, label, is_completed)
- VALUES (:occurrence_id, :position, :label, :is_completed)'
+ (occurrence_id, chapter_key, position, label, is_completed)
+ VALUES (:occurrence_id, :chapter_key, :position, :label, :is_completed)'
);
+ $chapterKeys = array_fill_keys(array_column(MARKET_OCCURRENCE_CHECKLIST_CHAPTERS, 'key'), true);
+ $chapterPositions = [];
foreach (array_values($items) as $position => $item) {
if (!is_array($item)) {
throw new InvalidArgumentException('Jeder Checklistenpunkt muss ein Objekt sein.');
}
+ $chapterKey = $item['chapter_key'] ?? null;
+ if (!is_string($chapterKey) || !isset($chapterKeys[$chapterKey])) {
+ throw new InvalidArgumentException('chapter_key verweist nicht auf ein festes Checklisten-Kapitel.');
+ }
$label = $item['label'] ?? '';
if (!is_string($label)) {
throw new InvalidArgumentException('Der Text eines Checklistenpunkts muss ein String sein.');
@@ -121,7 +160,9 @@ function market_management_update_occurrence_checklist(PDO $pdo, int $occurrence
throw new InvalidArgumentException('is_completed muss boolean sein.');
}
$insert->bindValue(':occurrence_id', $occurrenceId, PDO::PARAM_INT);
- $insert->bindValue(':position', $position + 1, PDO::PARAM_INT);
+ $chapterPositions[$chapterKey] = ($chapterPositions[$chapterKey] ?? 0) + 1;
+ $insert->bindValue(':chapter_key', $chapterKey, PDO::PARAM_STR);
+ $insert->bindValue(':position', $chapterPositions[$chapterKey], PDO::PARAM_INT);
$insert->bindValue(':label', trim($label), PDO::PARAM_STR);
$insert->bindValue(':is_completed', $completed, PDO::PARAM_BOOL);
$insert->execute();
diff --git a/modules/shared/auth/ui/home.php b/modules/shared/auth/ui/home.php
index 12ced9b..27f59f5 100644
--- a/modules/shared/auth/ui/home.php
+++ b/modules/shared/auth/ui/home.php
@@ -1203,8 +1203,9 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM
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 = ''; 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 = '
' + marketDisplay(checklist.comment) + '
Keine Markttage vorhanden.
'); };"; echo " const reorderMarketStatusCard = (html) => { const template = document.createElement('template'); template.innerHTML = html; const target = template.content.querySelector('[data-market-delete-occurrence-id]'); const cards = target ? Array.from(target.children).filter((child) => child.matches('.market-detail-card')) : []; if (cards.length > 1) { target.insertBefore(cards[0], cards[1].nextSibling); } return template.innerHTML; };"; echo " const renderMarketDetailWithActions = (payload) => reorderMarketStatusCard(marketDeletePatternWithTitleAndDays(payload.occurrence?.id || '', marketDrawerTitle(payload), renderMarketDetail(payload)));"; - echo " const ensureChecklistTrailingItem = (form) => { const list = form?.querySelector('[data-checklist-edit-list]'); if (!list) { return; } const trailing = list.querySelector('[data-checklist-trailing-item]'); if (!trailing || (trailing.querySelector('[data-checklist-label]')?.value || '').trim() !== '') { const item = document.createElement('label'); item.className = 'sg-labeled-input-row market-checklist-edit-row market-checklist-edit-row--trailing'; item.dataset.checklistEditItem = ''; item.dataset.checklistTrailingItem = ''; item.innerHTML = ''; list.append(item); } };"; + echo " const ensureChecklistTrailingItem = (form, list) => { const targetList = list || form?.querySelector('[data-checklist-edit-list]'); if (!targetList) { return; } const trailing = targetList.querySelector('[data-checklist-trailing-item]'); if (!trailing || (trailing.querySelector('[data-checklist-label]')?.value || '').trim() !== '') { const item = document.createElement('label'); item.className = 'sg-labeled-input-row market-checklist-edit-row market-checklist-edit-row--trailing'; item.dataset.checklistEditItem = ''; item.dataset.checklistTrailingItem = ''; item.dataset.checklistChapterKey = targetList.dataset.checklistChapterKey || ''; item.innerHTML = ''; targetList.append(item); } };"; echo " const setChecklistCheckbox = (button, checked) => { if (!button) { return; } button.setAttribute('aria-checked', String(checked)); button.classList.toggle('sg-form-active', checked); };"; - echo " const persistChecklistReadToggle = async (button) => { const drawer = getMarketDrawer(); const card = button.closest('[data-checklist-card]'); const status = card?.querySelector('[data-checklist-read-status]'); const index = Number(button.dataset.checklistReadCheckbox); let payload = {}; try { payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : {}; } catch (error) { return; } const sourceItems = Array.isArray(payload.occurrence?.checklist?.items) ? payload.occurrence.checklist.items : []; if (!sourceItems[index]) { return; } const previous = button.getAttribute('aria-checked') === 'true'; const next = !previous; setChecklistCheckbox(button, next); setChecklistCheckbox(card.querySelectorAll('[data-checklist-edit-checkbox]')[index], next); const items = Array.from(card.querySelectorAll('[data-checklist-read-checkbox]')).map((checkbox, itemIndex) => ({ label: sourceItems[itemIndex]?.label || '', is_completed: checkbox.getAttribute('aria-checked') === 'true' })); setChecklistSignal(card, items); button.disabled = true; if (status) { status.textContent = 'Speichern läuft …'; } try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'update_checklist', id: payload.occurrence.id, items: items.map((item) => ({ label: item.label || '', is_completed: Boolean(item.is_completed) })), comment: payload.occurrence.checklist.comment || '' }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } payload.occurrence.checklist = result.checklist; if (drawer) { drawer.dataset.marketPayload = JSON.stringify(payload); } if (status) { status.textContent = ''; } } catch (error) { setChecklistCheckbox(button, previous); setChecklistCheckbox(card.querySelectorAll('[data-checklist-edit-checkbox]')[index], previous); const rollbackItems = Array.from(card.querySelectorAll('[data-checklist-read-checkbox]')).map((checkbox, itemIndex) => ({ label: sourceItems[itemIndex]?.label || '', is_completed: checkbox.getAttribute('aria-checked') === 'true' })); setChecklistSignal(card, rollbackItems); if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'; } } finally { button.disabled = false; } };"; - echo " document.addEventListener('click', (event) => { const checklistOpen = event.target.closest('[data-checklist-open]'); if (checklistOpen) { const card = checklistOpen.closest('[data-checklist-card]'); const visible = !card?.classList.contains('market-checklist-card--collapsed'); setChecklistVisibility(card, !visible); if (!visible) { card?.querySelector('[data-checklist-read-checkbox]')?.focus(); } return; } const checklistEdit = event.target.closest('[data-checklist-card] [data-shared-editable-card-edit]'); if (checklistEdit) { const checklistCard = checklistEdit.closest('[data-checklist-card]'); setChecklistVisibility(checklistCard, true, false); setChecklistOpenButtonEnabled(checklistCard, false); return; } const checklistCancel = event.target.closest('[data-checklist-card] [data-shared-editable-card-cancel]'); if (checklistCancel) { const checklistCard = checklistCancel.closest('[data-checklist-card]'); setChecklistVisibility(checklistCard, true); setChecklistOpenButtonEnabled(checklistCard, true); return; } const checkbox = event.target.closest('[data-checklist-edit-checkbox]'); if (checkbox) { event.preventDefault(); setChecklistCheckbox(checkbox, checkbox.getAttribute('aria-checked') !== 'true'); const editCard = checkbox.closest('[data-checklist-card]'); setChecklistSignal(editCard, Array.from(editCard?.querySelectorAll('[data-checklist-edit-item]') || []).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' }))); return; } const readCheckbox = event.target.closest('[data-checklist-read-checkbox]'); if (readCheckbox) { event.preventDefault(); void persistChecklistReadToggle(readCheckbox); return; } const deleteItem = event.target.closest('[data-checklist-delete-item]'); if (deleteItem) { deleteItem.closest('[data-checklist-edit-item]')?.remove(); ensureChecklistTrailingItem(deleteItem.closest('[data-checklist-form]') || deleteItem.closest('[data-market-checklist-form]')); const deleteCard = deleteItem.closest('[data-checklist-card]'); setChecklistSignal(deleteCard, Array.from(deleteCard?.querySelectorAll('[data-checklist-edit-item]') || []).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' }))); } });"; - echo " document.addEventListener('input', (event) => { const input = event.target.closest('[data-market-checklist-form] [data-checklist-label]'); if (!input) { return; } const form = input.closest('[data-market-checklist-form]'); const item = input.closest('[data-checklist-edit-item]'); if (item?.hasAttribute('data-checklist-trailing-item') && input.value.trim() !== '') { item.removeAttribute('data-checklist-trailing-item'); } ensureChecklistTrailingItem(form); const card = input.closest('[data-checklist-card]'); setChecklistSignal(card, Array.from(card?.querySelectorAll('[data-checklist-edit-item]') || []).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' }))); });"; - echo " document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-market-checklist-form]'); if (!form) { return; } event.preventDefault(); event.stopImmediatePropagation(); const status = form.querySelector('[data-market-checklist-status]'); const checklistWasVisible = form.closest('[data-checklist-card]')?.classList.contains('market-checklist-card--collapsed') === false; const items = Array.from(form.querySelectorAll('[data-checklist-edit-item]')).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ label: item.querySelector('[data-checklist-label]')?.value || '', is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' })); if (status) { status.textContent = 'Speichern läuft …'; } try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'update_checklist', id: form.dataset.marketChecklistId, items, comment: form.querySelector('[data-checklist-comment]')?.value || '' }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } const trigger = contentRoot.querySelector('[data-market-drawer-open][aria-expanded=\"true\"]'); if (trigger) { await openMarketDrawer(trigger); if (checklistWasVisible) { setChecklistVisibility(getMarketDrawer()?.querySelector('[data-checklist-card]'), true); } } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'; } } });"; + echo " const persistChecklistReadToggle = async (button) => { const drawer = getMarketDrawer(); const card = button.closest('[data-checklist-card]'); const status = card?.querySelector('[data-checklist-read-status]'); const index = Number(button.dataset.checklistReadCheckbox); let payload = {}; try { payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : {}; } catch (error) { return; } const sourceItems = Array.isArray(payload.occurrence?.checklist?.items) ? payload.occurrence.checklist.items : []; if (!sourceItems[index]) { return; } const previous = button.getAttribute('aria-checked') === 'true'; const next = !previous; setChecklistCheckbox(button, next); setChecklistCheckbox(card.querySelectorAll('[data-checklist-edit-checkbox]')[index], next); const items = Array.from(card.querySelectorAll('[data-checklist-read-checkbox]')).map((checkbox, itemIndex) => ({ chapter_key: sourceItems[itemIndex]?.chapter_key || '', label: sourceItems[itemIndex]?.label || '', is_completed: checkbox.getAttribute('aria-checked') === 'true' })); setChecklistSignal(card, items); button.disabled = true; if (status) { status.textContent = 'Speichern läuft …'; } try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'update_checklist', id: payload.occurrence.id, items: items.map((item) => ({ chapter_key: item.chapter_key, label: item.label || '', is_completed: Boolean(item.is_completed) })), comment: payload.occurrence.checklist.comment || '' }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } payload.occurrence.checklist = result.checklist; if (drawer) { drawer.dataset.marketPayload = JSON.stringify(payload); } if (status) { status.textContent = ''; } } catch (error) { setChecklistCheckbox(button, previous); setChecklistCheckbox(card.querySelectorAll('[data-checklist-edit-checkbox]')[index], previous); const rollbackItems = Array.from(card.querySelectorAll('[data-checklist-read-checkbox]')).map((checkbox, itemIndex) => ({ chapter_key: sourceItems[itemIndex]?.chapter_key || '', label: sourceItems[itemIndex]?.label || '', is_completed: checkbox.getAttribute('aria-checked') === 'true' })); setChecklistSignal(card, rollbackItems); if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'; } } finally { button.disabled = false; } };"; + echo " document.addEventListener('click', (event) => { const checklistOpen = event.target.closest('[data-checklist-open]'); if (checklistOpen) { const card = checklistOpen.closest('[data-checklist-card]'); const visible = !card?.classList.contains('market-checklist-card--collapsed'); setChecklistVisibility(card, !visible); if (!visible) { card?.querySelector('[data-checklist-read-checkbox]')?.focus(); } return; } const checklistEdit = event.target.closest('[data-checklist-card] [data-shared-editable-card-edit]'); if (checklistEdit) { const checklistCard = checklistEdit.closest('[data-checklist-card]'); setChecklistVisibility(checklistCard, true, false); setChecklistOpenButtonEnabled(checklistCard, false); return; } const checklistCancel = event.target.closest('[data-checklist-card] [data-shared-editable-card-cancel]'); if (checklistCancel) { const checklistCard = checklistCancel.closest('[data-checklist-card]'); setChecklistVisibility(checklistCard, true); setChecklistOpenButtonEnabled(checklistCard, true); return; } const checkbox = event.target.closest('[data-checklist-edit-checkbox]'); if (checkbox) { event.preventDefault(); setChecklistCheckbox(checkbox, checkbox.getAttribute('aria-checked') !== 'true'); const editCard = checkbox.closest('[data-checklist-card]'); setChecklistSignal(editCard, Array.from(editCard?.querySelectorAll('[data-checklist-edit-item]') || []).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' }))); return; } const readCheckbox = event.target.closest('[data-checklist-read-checkbox]'); if (readCheckbox) { event.preventDefault(); void persistChecklistReadToggle(readCheckbox); return; } const deleteItem = event.target.closest('[data-checklist-delete-item]'); if (deleteItem) { const list = deleteItem.closest('[data-checklist-edit-list]'); deleteItem.closest('[data-checklist-edit-item]')?.remove(); ensureChecklistTrailingItem(deleteItem.closest('[data-market-checklist-form]'), list); const deleteCard = deleteItem.closest('[data-checklist-card]'); setChecklistSignal(deleteCard, Array.from(deleteCard?.querySelectorAll('[data-checklist-edit-item]') || []).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' }))); } });"; + echo " document.addEventListener('input', (event) => { const input = event.target.closest('[data-market-checklist-form] [data-checklist-label]'); if (!input) { return; } const form = input.closest('[data-market-checklist-form]'); const item = input.closest('[data-checklist-edit-item]'); const list = input.closest('[data-checklist-edit-list]'); if (item?.hasAttribute('data-checklist-trailing-item') && input.value.trim() !== '') { item.removeAttribute('data-checklist-trailing-item'); } ensureChecklistTrailingItem(form, list); const card = input.closest('[data-checklist-card]'); setChecklistSignal(card, Array.from(card?.querySelectorAll('[data-checklist-edit-item]') || []).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' }))); });"; + echo " document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-market-checklist-form]'); if (!form) { return; } event.preventDefault(); event.stopImmediatePropagation(); const status = form.querySelector('[data-market-checklist-status]'); const checklistWasVisible = form.closest('[data-checklist-card]')?.classList.contains('market-checklist-card--collapsed') === false; const items = Array.from(form.querySelectorAll('[data-checklist-edit-item]')).filter((item) => !item.hasAttribute('data-checklist-trailing-item')).map((item) => ({ chapter_key: item.dataset.checklistChapterKey || item.closest('[data-checklist-edit-list]')?.dataset.checklistChapterKey || '', label: item.querySelector('[data-checklist-label]')?.value || '', is_completed: item.querySelector('[data-checklist-edit-checkbox]')?.getAttribute('aria-checked') === 'true' })); if (status) { status.textContent = 'Speichern läuft …'; } try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'update_checklist', id: form.dataset.marketChecklistId, items, comment: form.querySelector('[data-checklist-comment]')?.value || '' }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } const trigger = contentRoot.querySelector('[data-market-drawer-open][aria-expanded=\"true\"]'); if (trigger) { await openMarketDrawer(trigger); if (checklistWasVisible) { setChecklistVisibility(getMarketDrawer()?.querySelector('[data-checklist-card]'), true); } } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'; } } });"; echo " const marketDrawerTitle = (payload) => { const marketName = payload.market?.name || 'Marktdurchführung'; const dates = (Array.isArray(payload.occurrence?.days) ? payload.occurrence.days : []).map((day) => marketDate(day.date)).filter(Boolean); return dates.length ? marketName + ', Marktdurchführung ' + dates.join(', ') : marketName; };"; echo " const openMarketDrawer = async (trigger) => { const drawer = getMarketDrawer(); if (!drawer) { return; } const title = drawer.querySelector('[data-market-drawer-title]'); const content = drawer.querySelector('[data-market-drawer-content]'); setMarketDrawerState(true); if (title) { title.textContent = 'Marktdurchführung'; } if (content) { content.innerHTML = 'Marktdetails werden geladen …
'; } 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 = '' + marketEscape(error.message) + '
'; } } 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 = 'Pflichtangaben werden geladen …
'; } 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 = '' + marketEscape(error.message) + '
'; } } };";
' + marketDisplay(checklist.comment) + '