From 6226f2a037f16318761b4fbe5804d7ab8a3d3c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Gla=CC=88ser?= Date: Sun, 20 Sep 2026 11:58:25 +0200 Subject: [PATCH] =?UTF-8?q?Kostenpositionen=20im=20Durchf=C3=BChrungsdrawe?= =?UTF-8?q?r=20bearbeiten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konzepte/market_management_hermes_mcp_api.md | 2 -- modules/market_management/service.php | 11 ----------- modules/shared/auth/ui/home.php | 4 +++- public/api/market-occurrence.php | 16 ++++++++++++++++ 4 files changed, 19 insertions(+), 14 deletions(-) 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 4faba4e..856cd80 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 @@ -185,8 +185,6 @@ Weitere mögliche Tagesfelder sind `stand_start_time`, `stand_end_time`, - Veranstalter werden am Markt über `organizer_id` gesetzt und von den Durchführungen geerbt. Marktdurchführungen können nur `contact_person_name`, `contact_email` und `contact_phone` ändern. -- `market_management.delete_organizer` - - erforderlich: `occurrence_id` - `market_management.create_cost_item` - erforderlich: `occurrence_id`, `cost_type`, `gross_amount`, `tax_status` - `tax_status`: `tax_free` oder `taxable` diff --git a/modules/market_management/service.php b/modules/market_management/service.php index e0fb088..30a8e06 100644 --- a/modules/market_management/service.php +++ b/modules/market_management/service.php @@ -1192,17 +1192,6 @@ function market_management_set_occurrence_contact(PDO $pdo, array $input): array return (array) $stmt->fetch(); } -function market_management_delete_organizer(PDO $pdo, int $occurrenceId): array -{ - $stmt = $pdo->prepare('DELETE FROM market_occurrence_organizer WHERE occurrence_id = :occurrence_id RETURNING id, occurrence_id'); - $stmt->execute([':occurrence_id' => $occurrenceId]); - $organizer = $stmt->fetch(); - if (!is_array($organizer)) { - throw new RuntimeException('Veranstalter nicht gefunden.'); - } - return $organizer; -} - function market_management_create_cost_item(PDO $pdo, array $input): array { $occurrenceId = market_management_require_positive_id($input['occurrence_id'] ?? null, 'occurrence_id'); diff --git a/modules/shared/auth/ui/home.php b/modules/shared/auth/ui/home.php index 1bc163c..de76d00 100644 --- a/modules/shared/auth/ui/home.php +++ b/modules/shared/auth/ui/home.php @@ -1184,9 +1184,11 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM echo " const marketDayDeleteObserver = new MutationObserver(() => enhanceMarketDayDeleteControls(contentRoot)); marketDayDeleteObserver.observe(contentRoot, { childList: true, subtree: true });"; echo " const municipalityFieldObserver = new MutationObserver(() => initializeMunicipalityFields(contentRoot)); municipalityFieldObserver.observe(contentRoot, { childList: true, subtree: true }); initializeMunicipalityFields(contentRoot);"; echo " const municipalityReadObserver = new MutationObserver(() => syncMunicipalityReadValue(contentRoot)); municipalityReadObserver.observe(contentRoot, { childList: true, subtree: true }); syncMunicipalityReadValue(contentRoot);"; + echo " const enhanceMarketCostItems = (root) => { const drawer = getMarketDrawer(); const payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : null; const items = Array.isArray(payload?.occurrence?.cost_items) ? payload.occurrence.cost_items : []; root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation]').forEach((form) => { if (form.dataset.marketCostItemsEnhanced === 'true') { return; } const field = form.querySelector('[name=cost_items_text]')?.closest('.sg-form-sections-card__field-group'); if (!field) { return; } const state = items.map((item) => Object.assign({}, item)); form._marketCostItems = state; field.innerHTML = '

'; const editor = field.querySelector('[data-market-cost-items-editor]'); const render = () => { editor.innerHTML = ''; form._marketCostItems.forEach((item, index) => { const row = document.createElement('div'); row.className = 'sg-form-sections-card__field-group'; row.dataset.marketCostItemRow = 'true'; row.dataset.marketCostItemId = item.id || ''; row.innerHTML = '
' + (item.id ? '' : '') + '
'; editor.appendChild(row); }); }; form._marketCostItemRender = render; render(); form.dataset.marketCostItemsEnhanced = 'true'; }); };"; + echo " const marketCostItemObserver = new MutationObserver(() => enhanceMarketCostItems(contentRoot)); marketCostItemObserver.observe(contentRoot, { childList: true, subtree: true }); enhanceMarketCostItems(contentRoot);"; echo " if (!marketsBindingsInstalled) {"; echo " marketsBindingsInstalled = true;"; - echo " document.addEventListener('click', (event) => { const dayAdd = event.target.closest('[data-market-day-add]'); if (dayAdd) { event.preventDefault(); addMarketDayDraft(); return; } const editButton = event.target.closest('[data-shared-editable-card-edit]'); if (editButton) { window.setTimeout(() => syncMarketDayAddButton(contentRoot), 0); return; } const cancelButton = event.target.closest('[data-shared-editable-card-cancel]'); if (cancelButton) { if (cancelButton.closest('[data-market-edit-form][data-market-edit-draft=\"true\"]')) { window.setTimeout(discardMarketDayDraft, 0); } else { window.setTimeout(() => syncMarketDayAddButton(contentRoot), 0); } } });"; + echo " document.addEventListener('click', async (event) => { const costAdd = event.target.closest('[data-market-cost-item-add]'); if (costAdd) { const form = costAdd.closest('[data-market-edit-form]'); if (form?._marketCostItems) { form._marketCostItems.push({ cost_type: '', description: '', gross_amount: '', tax_status: 'tax_free', tax_rate: '0.00' }); form._marketCostItemRender(); } return; } const costCancel = event.target.closest('[data-market-cost-item-cancel]'); if (costCancel) { const form = costCancel.closest('[data-market-edit-form]'); const row = costCancel.closest('[data-market-cost-item-row]'); const index = Array.from(form.querySelectorAll('[data-market-cost-item-row]')).indexOf(row); if (index >= 0) { form._marketCostItems.splice(index, 1); form._marketCostItemRender(); } return; } const costSave = event.target.closest('[data-market-cost-item-save]'); if (costSave) { const form = costSave.closest('[data-market-edit-form]'); const row = costSave.closest('[data-market-cost-item-row]'); const fields = Object.fromEntries(Array.from(row.querySelectorAll('[data-market-cost-field]')).map((input) => [input.dataset.marketCostField, input.value])); const itemId = row.dataset.marketCostItemId; const status = form.querySelector('[data-market-cost-item-status]'); 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: itemId ? 'update_cost_item' : 'create_cost_item', id: itemId || form.dataset.marketEditId, fields }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } const trigger = contentRoot.querySelector('[data-market-drawer-open][aria-expanded=\"true\"]'); if (trigger) { await openMarketDrawer(trigger); } } catch (error) { if (status) { status.textContent = marketEscape(error.message); } } return; } const costDelete = event.target.closest('[data-market-cost-item-delete]'); if (costDelete) { const form = costDelete.closest('[data-market-edit-form]'); const row = costDelete.closest('[data-market-cost-item-row]'); const itemId = row?.dataset.marketCostItemId || ''; if (!itemId || !window.confirm('Diese Kostenposition löschen?')) { return; } const status = form.querySelector('[data-market-cost-item-status]'); if (status) { status.textContent = 'Löschen 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: 'delete_cost_item', id: itemId, confirmation: 'DELETE' }) }); 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); } } return; } const dayAdd = event.target.closest('[data-market-day-add]'); if (dayAdd) { event.preventDefault(); addMarketDayDraft(); return; } const editButton = event.target.closest('[data-shared-editable-card-edit]'); if (editButton) { window.setTimeout(() => syncMarketDayAddButton(contentRoot), 0); return; } const cancelButton = event.target.closest('[data-shared-editable-card-cancel]'); if (cancelButton) { if (cancelButton.closest('[data-market-edit-form][data-market-edit-draft=\"true\"]')) { window.setTimeout(discardMarketDayDraft, 0); } else { window.setTimeout(() => syncMarketDayAddButton(contentRoot), 0); } } });"; echo " document.addEventListener('click', (event) => { const deleteDaySubmit = event.target.closest('[data-market-day-delete-submit]'); if (deleteDaySubmit) { void deleteMarketDay(deleteDaySubmit); return; } const createButton = event.target.closest('[data-market-occurrence-create]'); if (createButton) { event.preventDefault(); void openNewMarketDrawer(); return; } if (event.target.closest('[data-market-create-cancel]')) { closeMarketDrawer(); } });"; echo " document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-market-create-form]'); if (!form) { return; } event.preventDefault(); event.stopImmediatePropagation(); const status = form.querySelector('[data-market-create-status]'); const fields = Object.fromEntries(new FormData(form).entries()); if (status) { status.textContent = 'Speichern läuft …'; } try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'create', fields }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } const detailResponse = await fetch('/api/market-occurrence.php?id=' + encodeURIComponent(result.occurrence_id), { credentials: 'same-origin' }); const payload = await detailResponse.json(); if (!detailResponse.ok || !payload.ok) { throw new Error(payload.error || 'Die neue Durchführung konnte nicht geladen werden.'); } const drawer = getMarketDrawer(); const title = drawer?.querySelector('[data-market-drawer-title]'); const content = drawer?.querySelector('[data-market-drawer-content]'); if (drawer) { drawer.dataset.marketPayload = JSON.stringify(payload); } if (title) { title.textContent = marketDrawerTitle(payload); } if (content) { content.innerHTML = renderMarketDetailWithActions(payload); applyMarketTabs(content); syncMarketDayAddButton(content); } } catch (error) { if (status) { status.textContent = marketEscape(error.message); } } });"; echo " document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-market-edit-form][data-market-edit-draft=\"true\"]'); if (!form) { return; } event.preventDefault(); event.stopImmediatePropagation(); const status = form.querySelector('[data-market-edit-status]'); const fields = Object.fromEntries(new FormData(form).entries()); if (status) { status.textContent = 'Speichern läuft …'; } try { const response = await fetch('/api/market-occurrence.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': contentRoot.querySelector('[data-market-csrf-token]')?.dataset.marketCsrfToken || '' }, body: JSON.stringify({ action: 'create_day', id: form.dataset.marketOccurrenceId, fields }) }); const result = await response.json(); if (!response.ok || !result.ok) { throw new Error(result.error || 'Speichern fehlgeschlagen.'); } const trigger = contentRoot.querySelector('[data-market-drawer-open][aria-expanded=\"true\"]'); if (trigger) { await openMarketDrawer(trigger); } } catch (error) { if (status) { status.textContent = marketEscape(error.message); } } });"; diff --git a/public/api/market-occurrence.php b/public/api/market-occurrence.php index 0f83eba..7afda22 100644 --- a/public/api/market-occurrence.php +++ b/public/api/market-occurrence.php @@ -63,6 +63,22 @@ if ($requestMethod === 'POST') { $day = market_management_delete_day($pdo, $id); echo json_encode(['ok' => true, 'day' => $day]); exit; + } elseif (($payload['action'] ?? '') === 'create_cost_item') { + $fields['occurrence_id'] = $id; + $costItem = market_management_create_cost_item($pdo, $fields); + echo json_encode(['ok' => true, 'cost_item' => $costItem], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } elseif (($payload['action'] ?? '') === 'update_cost_item') { + $costItem = market_management_update_cost_item($pdo, $id, $fields); + echo json_encode(['ok' => true, 'cost_item' => $costItem], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } elseif (($payload['action'] ?? '') === 'delete_cost_item') { + if (($payload['confirmation'] ?? '') !== 'DELETE') { + throw new InvalidArgumentException('Bestätigung muss exakt DELETE lauten.'); + } + $costItem = market_management_delete_cost_item($pdo, $id); + echo json_encode(['ok' => true, 'cost_item' => $costItem], 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)) {