diff --git a/db/migrations/0034_market_occurrence_checklist.sql b/db/migrations/0034_market_occurrence_checklist.sql
new file mode 100644
index 0000000..e9df1f5
--- /dev/null
+++ b/db/migrations/0034_market_occurrence_checklist.sql
@@ -0,0 +1,33 @@
+BEGIN;
+
+CREATE TABLE market_occurrence_checklist (
+ id BIGSERIAL PRIMARY KEY,
+ occurrence_id BIGINT NOT NULL UNIQUE REFERENCES market_occurrence(id) ON DELETE RESTRICT,
+ comment TEXT,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE market_occurrence_checklist_item (
+ id BIGSERIAL PRIMARY KEY,
+ occurrence_id BIGINT NOT NULL REFERENCES market_occurrence(id) ON DELETE RESTRICT,
+ position INTEGER NOT NULL,
+ label TEXT NOT NULL,
+ is_completed BOOLEAN NOT NULL DEFAULT FALSE,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ CONSTRAINT chk_market_occurrence_checklist_item_position CHECK (position > 0)
+);
+
+CREATE INDEX idx_market_occurrence_checklist_item_occurrence
+ ON market_occurrence_checklist_item(occurrence_id, position, id);
+
+INSERT INTO market_occurrence_checklist (occurrence_id)
+SELECT id FROM market_occurrence;
+
+INSERT INTO market_occurrence_checklist_item (occurrence_id, position, label)
+SELECT occurrence_id, position, 'Position ' || position
+FROM market_occurrence_checklist
+CROSS JOIN generate_series(1, 30) AS positions(position);
+
+COMMIT;
diff --git a/docs/modules/market_management/konzepte/market_management_db_ownership.md b/docs/modules/market_management/konzepte/market_management_db_ownership.md
index 7c97b85..9b677fe 100644
--- a/docs/modules/market_management/konzepte/market_management_db_ownership.md
+++ b/docs/modules/market_management/konzepte/market_management_db_ownership.md
@@ -13,6 +13,8 @@ Status: Verbindliches DB-Ownership-Konzept
| `market_occurrence_organizer` | `market_management` | Marktmanagement-Service über Kontakt-Schnittstelle |
| `market_occurrence_day` | `market_management` | Marktmanagement-Service |
| `market_occurrence_cost_item` | `market_management` | Marktmanagement-Service |
+| `market_occurrence_checklist` | `market_management` | Submodul `occurrence_checklist` |
+| `market_occurrence_checklist_item` | `market_management` | Submodul `occurrence_checklist` |
| `market_municipality_reference` | `market_management` | Referenzdatenprozess |
| `market_municipality` | `market_management` | Marktmanagement-Service |
| `market_mcp_idempotency` | `market_management` | MCP-Submodul |
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 595a594..4cf58d6 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
@@ -177,6 +177,15 @@ Kategorien anlegen oder ändern.
- erforderlich: `occurrence_id`
- liefert zusätzlich den Markt, Markttage, Kosten und
berechnete Tages- und Gesamterlöse
+- `market_management.get_occurrence_checklist`
+ - erforderlich: `occurrence_id`
+ - liefert die 30 initialen beziehungsweise individuell gepflegten Punkte
+ 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
- `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_hermes_mcp_konzept.md b/docs/modules/market_management/konzepte/market_management_hermes_mcp_konzept.md
index ea4211e..c2e5caf 100644
--- a/docs/modules/market_management/konzepte/market_management_hermes_mcp_konzept.md
+++ b/docs/modules/market_management/konzepte/market_management_hermes_mcp_konzept.md
@@ -36,6 +36,7 @@ Vorgesehene Toolgruppen:
- Marktdurchführungen suchen und lesen
- Marktdurchführungen anlegen, ändern, archivieren und reaktivieren
- Markttage anlegen, ändern und entfernen
+- Checklisten einer Marktdurchführung lesen und vollständig ändern
- Kategorien, Kantone und Gemeinden lesen
- Kosten und Zahlungsstatus ändern
- Übersichtsdaten lesen
@@ -67,6 +68,7 @@ Modulberechtigung.
- Jeder Tool-Aufruf ist atomar.
- Fehlgeschlagene Aufrufe erzeugen keine Teilpersistierung.
- Alle Schreibvorgänge laufen über die interne Modul-API.
+- Checklisten-Schreibvorgänge ersetzen die vollständige Checkliste atomar. Hermes darf Punkte ergänzen, entfernen, umbenennen, abhaken und zurücksetzen sowie den Checklistenkommentar ändern.
- Hermes darf bei einem neuen Markt nur eine bestehende Kategorie referenzieren.
- Wenn Hermes Informationen aus einer Datei verarbeitet und in einer
Marktdurchführung speichert, muss die Quelldatei als interner ``-Link im
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 c37502d..1a51a83 100644
--- a/docs/modules/market_management/konzepte/market_management_technisches_konzept.md
+++ b/docs/modules/market_management/konzepte/market_management_technisches_konzept.md
@@ -13,6 +13,8 @@ Tabellen:
- `market_occurrence_organizer`
- `market_occurrence_day`
- `market_occurrence_cost_item`
+- `market_occurrence_checklist`
+- `market_occurrence_checklist_item`
- `market_municipality_reference`
- `market_municipality`
- `market_mcp_idempotency`
@@ -31,6 +33,7 @@ Prozessbereiche:
- Umsatz- und Artikelmengen lesen und aggregieren
- Marktübersicht lesen
- Hermes-MCP-Zugriff ausführen
+- Marktdurchführungs-Checkliste verwalten
- Referenzdaten verwalten
## 2. Zweck und Scope
diff --git a/modules/market_management/mcp.php b/modules/market_management/mcp.php
index 1cac574..30cfce5 100644
--- a/modules/market_management/mcp.php
+++ b/modules/market_management/mcp.php
@@ -87,6 +87,8 @@ function market_management_mcp_tools(): array
['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.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', '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']]]],
@@ -104,6 +106,7 @@ function market_management_mcp_is_write_tool(string $tool): bool
'market_management.create_municipality',
'market_management.archive_market', 'market_management.reactivate_market',
'market_management.create_occurrence', 'market_management.update_occurrence',
+ 'market_management.update_occurrence_checklist',
'market_management.archive_occurrence', 'market_management.reactivate_occurrence',
'market_management.create_day', 'market_management.update_day', 'market_management.delete_day',
], true);
@@ -303,6 +306,16 @@ function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): a
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
(bool) ($arguments['include_archived'] ?? false)
),
+ 'market_management.get_occurrence_checklist' => market_management_get_occurrence_checklist(
+ $pdo,
+ market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
+ (bool) ($arguments['include_archived'] ?? false)
+ ),
+ 'market_management.update_occurrence_checklist' => market_management_update_occurrence_checklist(
+ $pdo,
+ market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
+ $arguments
+ ),
'market_management.update_occurrence' => market_management_mcp_update_occurrence(
$pdo,
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
diff --git a/modules/market_management/occurrence_checklist.php b/modules/market_management/occurrence_checklist.php
new file mode 100644
index 0000000..2bdb47c
--- /dev/null
+++ b/modules/market_management/occurrence_checklist.php
@@ -0,0 +1,139 @@
+prepare('SELECT id FROM market_occurrence WHERE id = :id AND ' . $predicate);
+ $stmt->execute([':id' => $occurrenceId]);
+ if ($stmt->fetchColumn() === false) {
+ throw new InvalidArgumentException('occurrence_id verweist nicht auf eine Marktdurchführung.');
+ }
+}
+
+function market_management_checklist_initialize(PDO $pdo, int $occurrenceId): void
+{
+ market_management_checklist_assert_occurrence($pdo, $occurrenceId, true);
+ $parent = $pdo->prepare(
+ 'INSERT INTO market_occurrence_checklist (occurrence_id)
+ VALUES (:occurrence_id)
+ ON CONFLICT (occurrence_id) DO NOTHING'
+ );
+ $parent->execute([':occurrence_id' => $occurrenceId]);
+
+ $existing = $pdo->prepare('SELECT COUNT(*) FROM market_occurrence_checklist_item WHERE occurrence_id = :occurrence_id');
+ $existing->execute([':occurrence_id' => $occurrenceId]);
+ if ((int) $existing->fetchColumn() > 0) {
+ return;
+ }
+
+ $item = $pdo->prepare(
+ 'INSERT INTO market_occurrence_checklist_item
+ (occurrence_id, position, label, is_completed)
+ VALUES (:occurrence_id, :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();
+ }
+}
+
+function market_management_get_occurrence_checklist(PDO $pdo, int $occurrenceId, bool $includeArchived = false): array
+{
+ market_management_checklist_assert_occurrence($pdo, $occurrenceId, $includeArchived);
+ $parent = $pdo->prepare(
+ 'SELECT id, occurrence_id, comment, created_at, updated_at
+ FROM market_occurrence_checklist
+ WHERE occurrence_id = :occurrence_id'
+ );
+ $parent->execute([':occurrence_id' => $occurrenceId]);
+ $checklist = $parent->fetch();
+ if (!is_array($checklist)) {
+ throw new RuntimeException('Checkliste der Marktdurchführung fehlt.');
+ }
+ $items = $pdo->prepare(
+ 'SELECT id, occurrence_id, position, label, is_completed, created_at, updated_at
+ FROM market_occurrence_checklist_item
+ WHERE occurrence_id = :occurrence_id
+ ORDER BY position ASC, id ASC'
+ );
+ $items->execute([':occurrence_id' => $occurrenceId]);
+ $checklist['items'] = $items->fetchAll();
+ return $checklist;
+}
+
+function market_management_update_occurrence_checklist(PDO $pdo, int $occurrenceId, array $input): array
+{
+ market_management_checklist_assert_occurrence($pdo, $occurrenceId, true);
+ $items = $input['items'] ?? null;
+ if (!is_array($items)) {
+ throw new InvalidArgumentException('items muss eine Liste sein.');
+ }
+ $comment = $input['comment'] ?? null;
+ if ($comment !== null && !is_string($comment)) {
+ throw new InvalidArgumentException('comment muss ein String oder null sein.');
+ }
+
+ $startedTransaction = !$pdo->inTransaction();
+ if ($startedTransaction) {
+ $pdo->beginTransaction();
+ }
+ try {
+ $parent = $pdo->prepare(
+ 'INSERT INTO market_occurrence_checklist (occurrence_id, comment)
+ VALUES (:occurrence_id, :comment)
+ ON CONFLICT (occurrence_id) DO UPDATE
+ SET comment = EXCLUDED.comment, updated_at = NOW()'
+ );
+ $parent->execute([':occurrence_id' => $occurrenceId, ':comment' => $comment === null ? null : trim($comment)]);
+
+ $delete = $pdo->prepare('DELETE FROM market_occurrence_checklist_item WHERE occurrence_id = :occurrence_id');
+ $delete->execute([':occurrence_id' => $occurrenceId]);
+ $insert = $pdo->prepare(
+ 'INSERT INTO market_occurrence_checklist_item
+ (occurrence_id, position, label, is_completed)
+ VALUES (:occurrence_id, :position, :label, :is_completed)'
+ );
+ foreach (array_values($items) as $position => $item) {
+ if (!is_array($item)) {
+ throw new InvalidArgumentException('Jeder Checklistenpunkt muss ein Objekt sein.');
+ }
+ $label = $item['label'] ?? '';
+ if (!is_string($label)) {
+ throw new InvalidArgumentException('Der Text eines Checklistenpunkts muss ein String sein.');
+ }
+ $completed = $item['is_completed'] ?? false;
+ if (!is_bool($completed)) {
+ throw new InvalidArgumentException('is_completed muss boolean sein.');
+ }
+ $insert->bindValue(':occurrence_id', $occurrenceId, PDO::PARAM_INT);
+ $insert->bindValue(':position', $position + 1, PDO::PARAM_INT);
+ $insert->bindValue(':label', trim($label), PDO::PARAM_STR);
+ $insert->bindValue(':is_completed', $completed, PDO::PARAM_BOOL);
+ $insert->execute();
+ }
+ if ($startedTransaction) {
+ $pdo->commit();
+ }
+ } catch (Throwable $error) {
+ if ($startedTransaction && $pdo->inTransaction()) {
+ $pdo->rollBack();
+ }
+ throw $error;
+ }
+ return market_management_get_occurrence_checklist($pdo, $occurrenceId, true);
+}
diff --git a/modules/market_management/service.php b/modules/market_management/service.php
index ddf14a9..5d7dd8f 100644
--- a/modules/market_management/service.php
+++ b/modules/market_management/service.php
@@ -2,6 +2,7 @@
declare(strict_types=1);
require_once __DIR__ . '/../shared/db.php';
+require_once __DIR__ . '/occurrence_checklist.php';
const MARKET_MANAGEMENT_STATUSES = [
'open',
@@ -956,6 +957,7 @@ function market_management_create_occurrence(PDO $pdo, array $input): array
'tax_status' => $taxStatus,
'tax_rate' => $taxRate,
]);
+ market_management_checklist_initialize($pdo, (int) $occurrence['id']);
if ($startedTransaction) {
$pdo->commit();
}
@@ -1132,6 +1134,7 @@ function market_management_get_occurrence(PDO $pdo, int $occurrenceId, bool $inc
? null
: round((float) $summary['gross_amount'] - (float) $occurrence['paid_amount'], 2),
];
+ $occurrence['checklist'] = market_management_get_occurrence_checklist($pdo, $occurrenceId, $includeArchived);
return $occurrence;
}
diff --git a/modules/shared/auth/ui/home.php b/modules/shared/auth/ui/home.php
index 35a2bf1..a41dae7 100644
--- a/modules/shared/auth/ui/home.php
+++ b/modules/shared/auth/ui/home.php
@@ -1202,6 +1202,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 = '
Keine Markttage vorhanden.
'); };"; + 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 ? '' + marketEscape(market.website_url) + '' : 'Nicht hinterlegt'; const contract = occurrence.contract_folder_url ? 'Vertragsordner öffnen' : 'Nicht hinterlegt'; const overview = marketRow('Markt', market.name) + marketRow('Kategorie', market.category_name || market.category_code) + '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('div'); item.className = 'market-checklist-edit-item market-checklist-edit-item--trailing'; item.dataset.checklistEditItem = ''; item.dataset.checklistTrailingItem = ''; item.innerHTML = ''; list.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 items = Array.isArray(payload.occurrence?.checklist?.items) ? payload.occurrence.checklist.items : []; if (!items[index]) { return; } const previous = Boolean(items[index].is_completed); const next = !previous; items[index].is_completed = next; setChecklistCheckbox(button, next); 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) { items[index].is_completed = previous; setChecklistCheckbox(button, previous); 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) { checklistOpen.closest('[data-checklist-card]')?.querySelector('[data-checklist-read-checkbox]')?.focus(); return; } const checkbox = event.target.closest('[data-checklist-edit-checkbox]'); if (checkbox) { event.preventDefault(); setChecklistCheckbox(checkbox, 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]')); } });"; + 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); });"; + 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 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); } } 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) + '
'; } } };"; diff --git a/public/api/market-occurrence.php b/public/api/market-occurrence.php index dd7c0fc..dd05fa3 100644 --- a/public/api/market-occurrence.php +++ b/public/api/market-occurrence.php @@ -63,6 +63,13 @@ if ($requestMethod === 'POST') { $day = market_management_delete_day($pdo, $id); echo json_encode(['ok' => true, 'day' => $day]); exit; + } elseif (($payload['action'] ?? '') === 'update_checklist') { + $checklist = market_management_update_occurrence_checklist($pdo, $id, [ + 'items' => $payload['items'] ?? null, + 'comment' => $payload['comment'] ?? null, + ]); + echo json_encode(['ok' => true, 'checklist' => $checklist], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; } elseif ($scope === 'day') { market_management_update_day($pdo, $id, $fields); } elseif (in_array($scope, ['occurrence', 'status', 'overview', 'participation', 'infrastructure', 'travel', 'notes'], true)) { diff --git a/public/assets/styles.portal.css b/public/assets/styles.portal.css index 0264cf7..8ee9008 100644 --- a/public/assets/styles.portal.css +++ b/public/assets/styles.portal.css @@ -431,6 +431,36 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) { color: var(--color-white); } +.market-checklist-list, +.market-checklist-edit-list { + display: flex; + flex-direction: column; + gap: var(--spacing-small); +} + +.market-checklist-item, +.market-checklist-edit-item { + display: flex; + align-items: center; + gap: var(--spacing-small); +} + +.market-checklist-edit-item [data-checklist-label] { + flex: 1 1 auto; + min-width: 0; +} + +.market-checklist-delete { + min-width: var(--small-interaction-element-size); + width: var(--small-interaction-element-size); + padding-inline: 0; +} + +.market-checklist-comment, +.market-checklist-comment-field { + margin-top: var(--spacing-large); +} + .market-required-field-indicator { position: relative; top: -0.25em; diff --git a/scripts/tests/market_management_mcp_tools_list_test.php b/scripts/tests/market_management_mcp_tools_list_test.php index 94e9c7b..6f653ef 100644 --- a/scripts/tests/market_management_mcp_tools_list_test.php +++ b/scripts/tests/market_management_mcp_tools_list_test.php @@ -58,6 +58,8 @@ $expected = [ 'market_management.get_overview' => 'Marktübersicht mit serverseitigen Filtern lesen', 'market_management.create_occurrence' => 'Marktdurchführung mit mindestens einem Markttag anlegen', 'market_management.get_occurrence' => 'Marktdurchführung mit Markt, Markttagen und berechneten Verkaufsdaten lesen', + 'market_management.get_occurrence_checklist' => 'Checkliste einer Marktdurchführung lesen', + 'market_management.update_occurrence_checklist' => 'Checkliste einer Marktdurchführung vollständig ersetzen', 'market_management.update_occurrence' => 'Marktdurchführung ändern', 'market_management.archive_occurrence' => 'Marktdurchführung archivieren', 'market_management.reactivate_occurrence' => 'Marktdurchführung reaktivieren', @@ -67,7 +69,7 @@ $expected = [ ]; $tools = market_management_mcp_tools(); -assert_test(count($tools) === 21, 'Expected 21 MCP tools, got ' . count($tools)); +assert_test(count($tools) === 23, 'Expected 23 MCP tools, got ' . count($tools)); $occurrenceTool = array_values(array_filter($tools, static fn (array $tool): bool => $tool['name'] === 'market_management.update_occurrence'))[0] ?? null; assert_test(is_array($occurrenceTool), 'Update occurrence MCP tool must exist');
' + marketDisplay(checklist.comment) + '