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 = '
' + title + '
' + (actions ? '
' + actions + '
' : '') + '
'; return '
' + header + '
' + body + '
' + (scope ? '' : '') + '
'; };"; - echo " const marketChecklistReadBody = (checklist) => { const items = Array.isArray(checklist.items) ? checklist.items : []; const rows = items.map((item, index) => '
' + marketEscape(item.label || '') + '
').join(''); return '
' + rows + '
Kommentar

' + marketDisplay(checklist.comment) + '

'; };"; - echo " const marketChecklistEditBody = (checklist) => { const items = Array.isArray(checklist.items) ? checklist.items : []; const rows = items.map((item) => '').join(''); return '
' + rows + '
'; };"; + echo " const marketChecklistChapters = (checklist) => Array.isArray(checklist.chapters) ? checklist.chapters : [];"; + echo " const marketChecklistReadBody = (checklist) => { const items = Array.isArray(checklist.items) ? checklist.items : []; const chapterSections = marketChecklistChapters(checklist).map((chapter) => { const chapterItems = items.filter((item) => item.chapter_key === chapter.key); const rows = chapterItems.map((item) => { const index = items.indexOf(item); return '
' + marketEscape(item.label || '') + '
'; }).join(''); return '

' + marketEscape(chapter.label) + '

' + rows + '
'; }).join(''); return '
' + chapterSections + '
Kommentar

' + marketDisplay(checklist.comment) + '

'; };"; + echo " const marketChecklistEditBody = (checklist) => { const items = Array.isArray(checklist.items) ? checklist.items : []; const sections = marketChecklistChapters(checklist).map((chapter) => { const chapterItems = items.filter((item) => item.chapter_key === chapter.key); const rows = chapterItems.map((item) => '').join(''); const trailing = ''; return '

' + marketEscape(chapter.label) + '

' + rows + trailing + '
'; }).join(''); return sections + ''; };"; echo " const marketChecklistCard = (id, checklist) => { const headerClass = marketChecklistSignalSegment(checklist.items); const actions = '' + marketEditButton('checklist', id); return '
Checkliste
' + actions + '
'; };"; echo " const setChecklistVisibility = (card, visible, showRead = visible) => { if (!card) { return; } card.classList.toggle('market-checklist-card--collapsed', !visible); const readView = card.querySelector('[data-shared-editable-card-read]'); const toggle = card.querySelector('[data-checklist-open]'); if (readView) { readView.hidden = !showRead; } if (toggle) { toggle.textContent = visible ? 'Verbergen' : 'Anzeigen'; toggle.setAttribute('aria-expanded', String(visible)); } };"; echo " const setChecklistOpenButtonEnabled = (card, enabled) => { const toggle = card?.querySelector('[data-checklist-open]'); if (!toggle) { return; } toggle.disabled = !enabled; toggle.setAttribute('aria-disabled', String(!enabled)); toggle.classList.toggle('sg-button--inactive', !enabled); };"; @@ -1242,12 +1243,12 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM 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) + '
Website
' + website + '
' + marketRow('Ort', occurrence.place) + marketRow('Adresse', occurrence.address) + marketRow('Gemeinde', occurrence.municipality_reference_id) + marketRow('Veranstalter', organization.name) + marketRow('Kontaktperson', marketContactName(contactPerson)) + marketRow('Kontakt E-Mail', contactPerson.email) + marketRow('Kontakt Telefon', contactPerson.phone); const dates = days.map((day) => marketDate(day.date)).filter((date) => date !== 'Nicht hinterlegt').join(' – '); const costSummary = occurrence.cost_summary || {}; const participation = marketRow('Anmeldung / Anfrage', occurrence.request_or_registration_date) + marketRow('Hinweis Anmeldung', occurrence.registration_note) + marketRow('Anmeldefrist', occurrence.registration_deadline) + marketRow('Bestätigung', occurrence.confirmation_date) + marketRow('Zahlungsfrist', occurrence.payment_deadline) + marketRow('Zahlungsstatus', occurrence.payment_status) + marketRow('Zahlungsdatum', occurrence.payment_date) + marketRow('Bezahlter Betrag', marketMoney(occurrence.paid_amount)) + marketRow('Zahlungsart', occurrence.payment_method_code) + marketRow('Gesamtkosten brutto', marketMoney(costSummary.gross_amount)) + marketRow('Gesamtkosten netto', marketMoney(costSummary.net_amount)) + marketRow('Steuern', marketMoney(costSummary.tax_amount)) + marketRow('Offener Betrag', marketMoney(costSummary.open_amount)) + '
Kostenpositionen
' + marketDisplay(occurrence.cost_items_text) + '
'; const infrastructure = marketRow('Standgrösse', occurrence.stand_size) + marketRow('Tisch vorhanden', marketBoolean(occurrence.table_available)) + marketRow('Tisch mietbar', marketBoolean(occurrence.table_rentable)) + marketRow('Eigener Tisch erforderlich', marketBoolean(occurrence.own_table_required)) + marketRow('Überdachung', marketBoolean(occurrence.shelter_available)) + marketRow('Strom', marketBoolean(occurrence.power_available)) + marketRow('Wasser', marketBoolean(occurrence.water_available)) + marketRow('Beleuchtung', marketBoolean(occurrence.lighting_available)) + marketRow('Weitere Infrastruktur', occurrence.other_infrastructure) + marketRow('Anforderungen an den eigenen Stand', occurrence.own_stand_requirements); const travel = marketRow('Reiseadresse', occurrence.travel_address) + marketRow('Distanz (km)', occurrence.travel_distance_km) + marketRow('Parkmöglichkeiten', occurrence.parking_options) + marketRow('Anlieferung', occurrence.delivery_access) + marketRow('Direktes Entladen', marketBoolean(occurrence.direct_unload_possible)) + marketRow('Lagerung', occurrence.storage_options) + marketRow('Besondere Anreisehinweise', occurrence.travel_notes); const notes = marketRow('Teilnahmebedingungen', occurrence.other_participation_conditions) + marketRow('Rücktrittsbedingungen', occurrence.cancellation_terms) + marketRow('Umsatzbeteiligung', occurrence.revenue_share_terms) + marketRow('Notizen', occurrence.notes) + '
Vertragsordner
' + contract + '
'; const salesSummary = occurrence.sales_summary || {}; const dayTabs = days.map((day, index) => { const label = day.__draft ? 'Neuer Tag' : marketDate(day.date); const selected = day.__draft || (!hasDraftDay && index === 0); return ''; }).join(''); const addDayTab = ''; const dayPanels = days.map((day, index) => { const label = day.__draft ? 'Neuer Tag' : marketDate(day.date); const selected = day.__draft || (!hasDraftDay && index === 0); const values = day.__draft ? Object.assign({}, day, { __occurrence_id: occurrence.id }) : day; return '
' + marketCard(label, '
' + marketRow('Datum', marketDate(day.date), true) + marketRow('Beobachtungen', day.observations, true) + marketRow('Standzeit von', day.stand_start_time, true) + marketRow('Standzeit bis', day.stand_end_time, true) + marketRow('Aufbau von', day.setup_start_time, true) + marketRow('Aufbau bis', day.setup_end_time, true) + marketRow('Anlieferung von', day.delivery_start_time, true) + marketRow('Anlieferung bis', day.delivery_end_time, true) + marketRow('Umsatz', marketMoney(day.gross_revenue), true) + marketRow('Verkaufte Artikel', day.sold_item_count, true) + '
', 'sg-card-segment--darkgreen', marketEditButton('day', day.id), 'day', day.id, values) + '
'; }).join(''); const salesCard = marketCard('Umsatz und Artikel', '
' + marketRow('Gesamterlös', marketMoney(salesSummary.gross_revenue)) + marketRow('Gesamtzahl verkaufter Artikel', salesSummary.sold_item_count) + marketRow('Durchschnittlicher Erlös pro Tag', marketMoney(salesSummary.average_revenue_per_day)) + marketRow('Durchschnittlicher Erlös pro verkauftem Artikel', marketMoney(salesSummary.average_revenue_per_item)) + '
', 'sg-card-segment--darkgreen'); return marketStatusCard(occurrence.id, occurrence) + marketCard('Markt und Marktdurchführung', '
' + overview + '
', 'sg-card-segment--darkgreen', marketEditButton('overview', occurrence.id), 'overview', occurrence.id, Object.assign({}, occurrence, { market_name: market.name, category_name: market.category_name || market.category_code, website_url: market.website_url, contact_email: contactPerson.email, contact_phone: contactPerson.phone })) + marketCard('Teilnahme und Zahlung', '
' + participation + '
', 'sg-card-segment--darkgreen', marketEditButton('participation', occurrence.id), 'participation', occurrence.id, occurrence) + marketCard('Infrastruktur', '
' + infrastructure + '
', 'sg-card-segment--darkgreen', marketEditButton('infrastructure', occurrence.id), 'infrastructure', occurrence.id, occurrence) + marketChecklistCard(occurrence.id, occurrence.checklist || {}) + marketCard('Anreise und Logistik', '
' + travel + '
', 'sg-card-segment--darkgreen', marketEditButton('travel', occurrence.id), 'travel', occurrence.id, occurrence) + salesCard + marketCard('Weitere Angaben', '
' + notes + '
', 'sg-card-segment--darkgreen', marketEditButton('notes', occurrence.id), 'notes', occurrence.id, occurrence) + (days.length ? '
' + dayTabs + addDayTab + '
' + dayPanels + '
' : '

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) + '

'; } } };";