diff --git a/db/migrations/0028_market_organizer_inheritance.sql b/db/migrations/0028_market_organizer_inheritance.sql new file mode 100644 index 0000000..57bb45d --- /dev/null +++ b/db/migrations/0028_market_organizer_inheritance.sql @@ -0,0 +1,66 @@ +BEGIN; + +ALTER TABLE party_role + DROP CONSTRAINT chk_party_role_code; + +ALTER TABLE party_role + ADD CONSTRAINT chk_party_role_code + CHECK (role_code IN ('manufacturer', 'market_organizer')); + +ALTER TABLE market_definition + ADD COLUMN organizer_party_id BIGINT; + +UPDATE market_definition m +SET organizer_party_id = source.organization_contact_id +FROM ( + SELECT o.market_definition_id, MIN(oo.organization_contact_id) AS organization_contact_id + FROM market_occurrence o + INNER JOIN market_occurrence_organizer oo ON oo.occurrence_id = o.id + GROUP BY o.market_definition_id +) source +WHERE source.market_definition_id = m.id; + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM market_definition WHERE organizer_party_id IS NULL) THEN + RAISE EXCEPTION 'Alle bestehenden Märkte benötigen vor der Migration einen Veranstalter.'; + END IF; +END $$; + +ALTER TABLE market_definition + ALTER COLUMN organizer_party_id SET NOT NULL, + ADD CONSTRAINT fk_market_definition_organizer_party + FOREIGN KEY (organizer_party_id) REFERENCES party(id) ON DELETE RESTRICT; + +CREATE INDEX idx_market_definition_organizer_party + ON market_definition(organizer_party_id); + +INSERT INTO party_role (party_id, role_code) +SELECT DISTINCT organizer_party_id, 'market_organizer' +FROM market_definition +ON CONFLICT (party_id, role_code) DO NOTHING; + +ALTER TABLE market_occurrence_organizer + ADD COLUMN contact_person_name TEXT, + ADD COLUMN contact_email TEXT, + ADD COLUMN contact_phone TEXT; + +UPDATE market_occurrence_organizer oo +SET contact_person_name = NULLIF(TRIM(CONCAT_WS(' ', c.first_name, c.last_name)), ''), + contact_email = c.email, + contact_phone = c.phone +FROM contact c +WHERE c.id = oo.contact_person_id; + +INSERT INTO market_occurrence_organizer (occurrence_id, contact_person_name, created_at, updated_at) +SELECT o.id, NULL, NOW(), NOW() +FROM market_occurrence o +WHERE NOT EXISTS ( + SELECT 1 FROM market_occurrence_organizer oo WHERE oo.occurrence_id = o.id +); + +ALTER TABLE market_occurrence_organizer + DROP COLUMN organization_contact_id, + DROP COLUMN contact_person_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 dfda3a4..9ede661 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 @@ -97,7 +97,12 @@ Das Ergebnis steht bei `tools/call` als JSON im Textinhalt des ersten - optional: `include_archived` - liefert Marktdurchführungen inklusive Markttagen, Kosten und berechneten Verkaufswerten - `market_management.create_market` - - erforderlich: `name`, `category_id` + - erforderlich: `name`, `category_id`, `organizer_party_id` +- `market_management.list_market_organizers` + - liefert aktive Veranstalterorganisationen +- `market_management.create_market_organizer` + - erforderlich: `name` + - optional: `email`, `phone` - optional: `website_url` als HTTP(S)-URL des Marktes - `market_management.update_market` - erforderlich: `market_id` @@ -173,9 +178,9 @@ Weitere mögliche Tagesfelder sind `stand_start_time`, `stand_end_time`, ### Veranstalter und Kosten -- `market_management.set_organizer` - - erforderlich: `occurrence_id`, `organization_contact_id`, - `contact_person_id` +- Veranstalter werden am Markt über `organizer_party_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` 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 2864f73..adccbfe 100644 --- a/docs/modules/market_management/konzepte/market_management_technisches_konzept.md +++ b/docs/modules/market_management/konzepte/market_management_technisches_konzept.md @@ -84,6 +84,7 @@ Normativ: - jeder Markt besitzt genau einen eindeutigen Namen - jeder Markt besitzt genau eine Kategorie +- jeder Markt besitzt genau einen verpflichtenden Veranstalter aus dem Kontaktmodul - jeder Markt kann eine optionale Website-URL der Veranstaltung besitzen - der Markt enthält keine Durchführungs-, Kontakt-, Kosten-, Zahlungs- oder Erlösdaten @@ -101,6 +102,7 @@ Normativ: Zahlungsdaten gehören zur Durchführung - der Status gehört zur Durchführung - Zahlungsstatus und Marktstatus sind unabhängig +- der Veranstalter wird von jeder Durchführung des Marktes geerbt und ist dort nicht editierbar - der Zahlungsstatus ist manuell `paid` oder `unpaid` Minimale Pflichtfelder über alle Statuswerte hinweg: @@ -115,10 +117,10 @@ Veranstalterbeziehung einer Durchführung. Normativ: -- der Veranstalter ist eine Organisation aus dem Kontaktmodul -- jede Durchführung besitzt höchstens genau einen Veranstalterdatensatz -- der Veranstalterdatensatz referenziert genau eine Kontaktperson -- Kontakt- und Organisationsdaten werden nicht im Marktmanagement dupliziert +- die Tabelle enthält die durchführungsbezogene Kontaktperson als Freitext sowie optionale E-Mail und Telefonnummer +- jede Durchführung besitzt genau einen lokalen Kontaktdatensatz +- der Veranstalter selbst wird ausschließlich über `market_definition.organizer_party_id` vom Markt geerbt +- Veranstalterorganisationen werden im Kontaktmodul über die Rolle `market_organizer` verwaltet ### 3.5 `market_occurrence_day` diff --git a/modules/erp/kontakte/service.php b/modules/erp/kontakte/service.php index f046e07..8a3f224 100644 --- a/modules/erp/kontakte/service.php +++ b/modules/erp/kontakte/service.php @@ -166,6 +166,76 @@ function get_party_options(PDO $pdo): array ); } +function get_market_organizer_options(PDO $pdo): array +{ + $stmt = $pdo->query( + "SELECT p.id, p.name, p.email, p.phone + FROM party p + INNER JOIN party_role pr ON pr.party_id = p.id AND pr.role_code = 'market_organizer' + WHERE p.status = 'active' + ORDER BY p.name ASC, p.id ASC" + ); + + return $stmt->fetchAll() ?: []; +} + +function get_market_organizer_reference(PDO $pdo, int $partyId): ?array +{ + $stmt = $pdo->prepare( + "SELECT p.id, p.name, p.email, p.phone + FROM party p + INNER JOIN party_role pr ON pr.party_id = p.id AND pr.role_code = 'market_organizer' + WHERE p.id = :id AND p.status = 'active'" + ); + $stmt->execute([':id' => $partyId]); + $party = $stmt->fetch(); + + return is_array($party) ? $party : null; +} + +function create_market_organizer(PDO $pdo, string $name, ?string $email = null, ?string $phone = null): array +{ + $name = trim($name); + $email = $email !== null ? trim($email) : null; + $phone = $phone !== null ? trim($phone) : null; + if ($name === '') { + throw new InvalidArgumentException('Veranstaltername fehlt.'); + } + + $pdo->beginTransaction(); + try { + $stmt = $pdo->prepare( + "SELECT id, name, email, phone FROM party WHERE LOWER(TRIM(name)) = LOWER(:name) LIMIT 1 FOR UPDATE" + ); + $stmt->execute([':name' => $name]); + $party = $stmt->fetch(); + if (!is_array($party)) { + $stmt = $pdo->prepare( + "INSERT INTO party (type, name, email, phone, status, created_at, updated_at) + VALUES ('customer', :name, :email, :phone, 'active', NOW(), NOW()) + RETURNING id, name, email, phone" + ); + $stmt->execute([':name' => $name, ':email' => $email ?: null, ':phone' => $phone ?: null]); + $party = $stmt->fetch(); + } + if (!is_array($party)) { + throw new RuntimeException('Veranstalter konnte nicht angelegt werden.'); + } + $role = $pdo->prepare( + "INSERT INTO party_role (party_id, role_code) VALUES (:party_id, 'market_organizer') + ON CONFLICT (party_id, role_code) DO NOTHING" + ); + $role->execute([':party_id' => (int) $party['id']]); + $pdo->commit(); + return (array) $party; + } catch (Throwable $error) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + throw $error; + } +} + function get_contact_options(PDO $pdo): array { $stmt = $pdo->query( diff --git a/modules/market_management/mcp.php b/modules/market_management/mcp.php index 73e1f90..98d8e80 100644 --- a/modules/market_management/mcp.php +++ b/modules/market_management/mcp.php @@ -51,7 +51,7 @@ function market_management_mcp_tools(): array 'travel_notes' => ['type' => ['string', 'null']], 'cancellation_terms' => ['type' => ['string', 'null']], 'revenue_share_terms' => ['type' => ['string', 'null']], 'other_participation_conditions' => ['type' => ['string', 'null']], 'contract_folder_url' => ['type' => ['string', 'null'], 'format' => 'uri'], - 'organization_contact_id' => ['type' => ['integer', 'null']], 'contact_person_id' => ['type' => ['integer', 'null']], + 'contact_person_name' => ['type' => ['string', 'null']], 'contact_email' => ['type' => ['string', 'null'], 'format' => 'email'], 'contact_phone' => ['type' => ['string', 'null']], ]; $dayProperties = [ @@ -73,11 +73,13 @@ function market_management_mcp_tools(): array ['name' => 'market_management.search_markets', 'description' => 'Märkte suchen', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]], ['name' => 'market_management.search_occurrences', 'description' => 'Marktdurchführungen suchen und auflisten', 'inputSchema' => ['type' => 'object', 'properties' => $searchProperties]], ['name' => 'market_management.get_market', 'description' => 'Markt mit Marktdurchführungen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer'], 'include_archived' => ['type' => 'boolean']]]], - ['name' => 'market_management.create_market', 'description' => 'Markt mit bestehender Kategorie und optionaler Website-URL anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'category_id'], 'properties' => ['name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]], - ['name' => 'market_management.update_market', 'description' => 'Markt inklusive Website-URL ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer'], 'name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]], + ['name' => 'market_management.create_market', 'description' => 'Markt mit verpflichtendem Veranstalter, Kategorie und optionaler Website-URL anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'category_id', 'organizer_party_id'], 'properties' => ['name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_party_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]], + ['name' => 'market_management.update_market', 'description' => 'Markt inklusive Veranstalter und Website-URL ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer'], 'name' => ['type' => 'string'], 'category_id' => ['type' => 'integer'], 'organizer_party_id' => ['type' => 'integer'], 'website_url' => ['type' => ['string', 'null'], 'format' => 'uri']]]], ['name' => 'market_management.archive_market', 'description' => 'Markt archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer']]]], ['name' => 'market_management.reactivate_market', 'description' => 'Markt reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id'], 'properties' => ['market_id' => ['type' => 'integer']]]], ['name' => 'market_management.list_categories', 'description' => 'Bestehende Kategorien lesen', 'inputSchema' => ['type' => 'object', 'properties' => []]], + ['name' => 'market_management.list_market_organizers', 'description' => 'Aktive Veranstalter lesen', 'inputSchema' => ['type' => 'object', 'properties' => []]], + ['name' => 'market_management.create_market_organizer', 'description' => 'Veranstalter mit optionalen Kontaktdaten anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name'], 'properties' => ['name' => ['type' => 'string'], 'email' => ['type' => ['string', 'null'], 'format' => 'email'], 'phone' => ['type' => ['string', 'null']]]]], ['name' => 'market_management.list_reference_data', 'description' => 'Kategorien, Kantone und Gemeinden lesen', 'inputSchema' => ['type' => 'object', 'properties' => []]], ['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]]], @@ -88,8 +90,6 @@ function market_management_mcp_tools(): array ['name' => 'market_management.create_day', 'description' => 'Markttag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'date'], 'properties' => ['occurrence_id' => ['type' => 'integer']] + $dayProperties]], ['name' => 'market_management.update_day', 'description' => 'Markttag ändern', 'inputSchema' => ['type' => 'object', 'required' => ['day_id'], 'properties' => ['day_id' => ['type' => 'integer']] + $dayProperties]], ['name' => 'market_management.delete_day', 'description' => 'Markttag entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['day_id'], 'properties' => ['day_id' => ['type' => 'integer']]]], - ['name' => 'market_management.set_organizer', 'description' => 'Veranstalter einer Marktdurchführung setzen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'organization_contact_id', 'contact_person_id'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'organization_contact_id' => ['type' => 'integer'], 'contact_person_id' => ['type' => 'integer']]]], - ['name' => 'market_management.delete_organizer', 'description' => 'Veranstalter einer Marktdurchführung entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id'], 'properties' => ['occurrence_id' => ['type' => 'integer']]]], ['name' => 'market_management.create_cost_item', 'description' => 'Kostenposition anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'cost_type', 'gross_amount', 'tax_status'], 'properties' => ['occurrence_id' => ['type' => 'integer'], 'cost_type' => ['type' => 'string'], 'description' => ['type' => ['string', 'null']], 'gross_amount' => ['type' => 'number'], 'tax_status' => ['type' => 'string', 'enum' => ['tax_free', 'taxable']], 'tax_rate' => ['type' => 'number']]]], ['name' => 'market_management.update_cost_item', 'description' => 'Kostenposition ändern', 'inputSchema' => ['type' => 'object', 'required' => ['cost_item_id'], 'properties' => ['cost_item_id' => ['type' => 'integer'], 'cost_type' => ['type' => 'string'], 'description' => ['type' => ['string', 'null']], 'gross_amount' => ['type' => ['number', 'null']], 'tax_status' => ['type' => ['string', 'null'], 'enum' => ['tax_free', 'taxable']], 'tax_rate' => ['type' => ['number', 'null']]]]], ['name' => 'market_management.delete_cost_item', 'description' => 'Kostenposition entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['cost_item_id'], 'properties' => ['cost_item_id' => ['type' => 'integer']]]], @@ -100,11 +100,11 @@ function market_management_mcp_is_write_tool(string $tool): bool { return in_array($tool, [ 'market_management.create_market', 'market_management.update_market', + 'market_management.create_market_organizer', 'market_management.archive_market', 'market_management.reactivate_market', 'market_management.create_occurrence', 'market_management.update_occurrence', 'market_management.archive_occurrence', 'market_management.reactivate_occurrence', 'market_management.create_day', 'market_management.update_day', 'market_management.delete_day', - 'market_management.set_organizer', 'market_management.delete_organizer', 'market_management.create_cost_item', 'market_management.update_cost_item', 'market_management.delete_cost_item', ], true); @@ -218,41 +218,22 @@ function market_management_mcp_get_market(PDO $pdo, int $marketId, bool $include function market_management_mcp_update_occurrence(PDO $pdo, int $occurrenceId, array $input): array { $contactFields = []; - foreach (['contact_email', 'contact_phone'] as $field) { + foreach (['contact_person_name', 'contact_email', 'contact_phone'] as $field) { if (array_key_exists($field, $input)) { - $contactFields[substr($field, 8)] = $input[$field]; - unset($input[$field]); - } - } - $organizerFields = []; - foreach (['organization_contact_id', 'contact_person_id'] as $field) { - if (array_key_exists($field, $input)) { - $organizerFields[$field] = $input[$field]; + $contactFields[$field] = $input[$field]; unset($input[$field]); } } $existing = null; - if ($contactFields !== [] || $organizerFields !== []) { + if ($contactFields !== []) { $existing = market_management_get_occurrence($pdo, $occurrenceId, true); if ($existing === null) { throw new RuntimeException('Marktdurchführung nicht gefunden.'); } } - if ($organizerFields !== []) { - $organizer = $existing['organizer'] ?? []; - $organizerFields['occurrence_id'] = $occurrenceId; - $organizerFields['organization_contact_id'] ??= $organizer['organization_contact_id'] ?? null; - $organizerFields['contact_person_id'] ??= $organizer['contact_person_id'] ?? null; - market_management_set_organizer($pdo, $organizerFields); - } if ($contactFields !== []) { - $contactId = (int) ($organizerFields['contact_person_id'] ?? $existing['contact_person_id'] ?? 0); - $hasContactValue = array_reduce($contactFields, static fn (bool $carry, mixed $value): bool => $carry || trim((string) $value) !== '', false); - if ($contactId > 0 && ($hasContactValue || array_key_exists('contact_person_id', $organizerFields))) { - update_contact_details($pdo, $contactId, $contactFields); - } elseif ($hasContactValue) { - throw new InvalidArgumentException('Für E-Mail oder Telefon muss eine Kontaktperson ausgewählt sein.'); - } + $contactFields['occurrence_id'] = $occurrenceId; + market_management_set_occurrence_contact($pdo, $contactFields); } if ($input !== []) { market_management_update_occurrence($pdo, $occurrenceId, $input); @@ -287,6 +268,13 @@ function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): a false ), 'market_management.list_categories' => market_management_list_categories($pdo), + 'market_management.list_market_organizers' => get_market_organizer_options($pdo), + 'market_management.create_market_organizer' => create_market_organizer( + $pdo, + (string) ($arguments['name'] ?? ''), + $arguments['email'] ?? null, + $arguments['phone'] ?? null + ), 'market_management.list_reference_data' => market_management_list_reference_data($pdo, $arguments), 'market_management.get_overview' => [ 'markets' => market_management_search_markets($pdo, $arguments), @@ -323,11 +311,6 @@ function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): a $pdo, market_management_require_positive_id($arguments['day_id'] ?? null, 'day_id') ), - 'market_management.set_organizer' => market_management_set_organizer($pdo, $arguments), - 'market_management.delete_organizer' => market_management_delete_organizer( - $pdo, - market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id') - ), 'market_management.create_cost_item' => market_management_create_cost_item($pdo, $arguments), 'market_management.update_cost_item' => market_management_update_cost_item( $pdo, diff --git a/modules/market_management/service.php b/modules/market_management/service.php index eb14ca1..1516058 100644 --- a/modules/market_management/service.php +++ b/modules/market_management/service.php @@ -212,6 +212,7 @@ function market_management_create_market(PDO $pdo, array $input): array { $name = trim((string) ($input['name'] ?? '')); $categoryId = market_management_require_positive_id($input['category_id'] ?? null, 'category_id'); + $organizerPartyId = market_management_require_positive_id($input['organizer_party_id'] ?? null, 'organizer_party_id'); $websiteUrl = market_management_optional_url($input, 'website_url'); if ($name === '') { @@ -220,13 +221,16 @@ function market_management_create_market(PDO $pdo, array $input): array if (!market_management_category_exists($pdo, $categoryId)) { throw new InvalidArgumentException('category_id verweist nicht auf eine aktive Kategorie.'); } + if (get_market_organizer_reference($pdo, $organizerPartyId) === null) { + throw new InvalidArgumentException('organizer_party_id verweist nicht auf einen aktiven Veranstalter.'); + } $stmt = $pdo->prepare( - 'INSERT INTO market_definition (name, category_id, website_url) - VALUES (:name, :category_id, :website_url) - RETURNING id, name, category_id, website_url, archived_at, created_at, updated_at' + 'INSERT INTO market_definition (name, category_id, organizer_party_id, website_url) + VALUES (:name, :category_id, :organizer_party_id, :website_url) + RETURNING id, name, category_id, organizer_party_id, website_url, archived_at, created_at, updated_at' ); - $stmt->execute([':name' => $name, ':category_id' => $categoryId, ':website_url' => $websiteUrl]); + $stmt->execute([':name' => $name, ':category_id' => $categoryId, ':organizer_party_id' => $organizerPartyId, ':website_url' => $websiteUrl]); return (array) $stmt->fetch(); } @@ -294,16 +298,18 @@ function market_management_search_markets(PDO $pdo, array $input): array } $stmt = $pdo->prepare( - 'SELECT m.id, m.name, m.category_id, m.website_url, c.code AS category_code, c.name AS category_name, + 'SELECT m.id, m.name, m.category_id, m.organizer_party_id, m.website_url, c.code AS category_code, c.name AS category_name, + p.name AS organizer_name, m.archived_at, m.created_at, m.updated_at, MIN(d.date) AS earliest_date, MAX(d.date) AS latest_date, COUNT(DISTINCT o.id) AS occurrence_count FROM market_definition m INNER JOIN market_category c ON c.id = m.category_id + INNER JOIN party p ON p.id = m.organizer_party_id LEFT JOIN market_occurrence o ON o.market_definition_id = m.id AND ' . $occurrenceArchivePredicate . ' LEFT JOIN market_occurrence_day d ON d.occurrence_id = o.id WHERE ' . ($where === [] ? 'TRUE' : implode(' AND ', $where)) . ' - GROUP BY m.id, m.category_id, c.code, c.name + GROUP BY m.id, m.category_id, m.organizer_party_id, c.code, c.name, p.name ORDER BY ' . $sortColumn . ' ' . $sortDirection . ', m.id ASC LIMIT :limit OFFSET :offset' ); @@ -318,10 +324,12 @@ function market_management_get_market(PDO $pdo, int $marketId, bool $includeArch { $marketArchivePredicate = $includeArchived ? 'TRUE' : 'm.archived_at IS NULL'; $stmt = $pdo->prepare( - 'SELECT m.id, m.name, m.category_id, m.website_url, c.code AS category_code, c.name AS category_name, + 'SELECT m.id, m.name, m.category_id, m.organizer_party_id, m.website_url, c.code AS category_code, c.name AS category_name, + p.name AS organizer_name, m.archived_at, m.created_at, m.updated_at FROM market_definition m INNER JOIN market_category c ON c.id = m.category_id + INNER JOIN party p ON p.id = m.organizer_party_id WHERE m.id = :id AND ' . $marketArchivePredicate ); $stmt->execute([':id' => $marketId]); @@ -359,7 +367,7 @@ function market_management_get_market(PDO $pdo, int $marketId, bool $includeArch function market_management_list_active_market_definitions(PDO $pdo): array { $stmt = $pdo->query( - 'SELECT m.id, m.name, m.category_id, c.name AS category_name + 'SELECT m.id, m.name, m.category_id, m.organizer_party_id, c.name AS category_name FROM market_definition m INNER JOIN market_category c ON c.id = m.category_id WHERE m.archived_at IS NULL @@ -493,6 +501,14 @@ function market_management_update_market(PDO $pdo, int $marketId, array $input): $sets[] = 'category_id = :category_id'; $params[':category_id'] = $categoryId; } + if (array_key_exists('organizer_party_id', $input)) { + $organizerPartyId = market_management_require_positive_id($input['organizer_party_id'], 'organizer_party_id'); + if (get_market_organizer_reference($pdo, $organizerPartyId) === null) { + throw new InvalidArgumentException('organizer_party_id verweist nicht auf einen aktiven Veranstalter.'); + } + $sets[] = 'organizer_party_id = :organizer_party_id'; + $params[':organizer_party_id'] = $organizerPartyId; + } if (array_key_exists('website_url', $input)) { $sets[] = 'website_url = :website_url'; $params[':website_url'] = market_management_optional_url($input, 'website_url'); @@ -504,7 +520,7 @@ function market_management_update_market(PDO $pdo, int $marketId, array $input): $sets[] = 'updated_at = NOW()'; $stmt = $pdo->prepare( 'UPDATE market_definition SET ' . implode(', ', $sets) . ' WHERE id = :id - RETURNING id, name, category_id, website_url, archived_at, created_at, updated_at' + RETURNING id, name, category_id, organizer_party_id, website_url, archived_at, created_at, updated_at' ); $stmt->execute($params); $market = $stmt->fetch(); @@ -520,7 +536,7 @@ function market_management_set_market_archive(PDO $pdo, int $marketId, bool $arc 'UPDATE market_definition SET archived_at = ' . ($archive ? 'COALESCE(archived_at, NOW())' : 'NULL') . ', updated_at = NOW() WHERE id = :id - RETURNING id, name, category_id, website_url, archived_at, created_at, updated_at' + RETURNING id, name, category_id, organizer_party_id, website_url, archived_at, created_at, updated_at' ); $stmt->execute([':id' => $marketId]); $market = $stmt->fetch(); @@ -621,6 +637,12 @@ function market_management_create_occurrence(PDO $pdo, array $input): array $day['occurrence_id'] = (int) $occurrence['id']; market_management_create_day($pdo, $day); } + market_management_set_occurrence_contact($pdo, [ + 'occurrence_id' => (int) $occurrence['id'], + 'contact_person_name' => null, + 'contact_email' => null, + 'contact_phone' => null, + ]); if ($startedTransaction) { $pdo->commit(); } @@ -724,18 +746,26 @@ function market_management_get_occurrence(PDO $pdo, int $occurrenceId, bool $inc $organizer->execute([':occurrence_id' => $occurrenceId]); $organizer = $organizer->fetch(); if (is_array($organizer)) { - $organizer['organization'] = get_party_reference($pdo, (int) $organizer['organization_contact_id']); - $organizer['contact_person'] = get_contact_reference($pdo, (int) $organizer['contact_person_id']); + $market = market_management_get_market($pdo, (int) $occurrence['market_definition_id'], true); + $organizer['organization'] = is_array($market) + ? get_market_organizer_reference($pdo, (int) $market['organizer_party_id']) + : null; + $organizer['contact_person'] = [ + 'first_name' => (string) ($organizer['contact_person_name'] ?? ''), + 'last_name' => '', + 'email' => (string) ($organizer['contact_email'] ?? ''), + 'phone' => (string) ($organizer['contact_phone'] ?? ''), + ]; } $occurrence['organizer'] = is_array($organizer) ? $organizer : null; - $occurrence['organization_contact_id'] = is_array($organizer) ? (int) $organizer['organization_contact_id'] : null; - $occurrence['contact_person_id'] = is_array($organizer) ? (int) $organizer['contact_person_id'] : null; + $occurrence['organizer_party_id'] = is_array($occurrence['organizer']) && is_array($occurrence['organizer']['organization'] ?? null) + ? (int) $occurrence['organizer']['organization']['id'] + : null; $referenceData = market_management_list_reference_data($pdo, []); $occurrence['reference_options'] = [ 'categories' => market_management_list_categories($pdo), 'municipalities' => $referenceData['municipalities'] ?? [], - 'parties' => get_party_options($pdo), - 'contacts' => get_contact_options($pdo), + 'market_organizers' => get_market_organizer_options($pdo), ]; $costs = $pdo->prepare('SELECT * FROM market_occurrence_cost_item WHERE occurrence_id = :occurrence_id ORDER BY id ASC'); $costs->execute([':occurrence_id' => $occurrenceId]); @@ -1005,24 +1035,33 @@ function market_management_delete_day(PDO $pdo, int $dayId): array return $day; } -function market_management_set_organizer(PDO $pdo, array $input): array +function market_management_set_occurrence_contact(PDO $pdo, array $input): array { $occurrenceId = market_management_require_positive_id($input['occurrence_id'] ?? null, 'occurrence_id'); - $organizationId = market_management_require_positive_id($input['organization_contact_id'] ?? null, 'organization_contact_id'); - $contactPersonId = market_management_require_positive_id($input['contact_person_id'] ?? null, 'contact_person_id'); + $name = array_key_exists('contact_person_name', $input) + ? market_management_optional_string($input, 'contact_person_name') + : null; + $email = array_key_exists('contact_email', $input) + ? market_management_optional_string($input, 'contact_email') + : null; + $phone = array_key_exists('contact_phone', $input) + ? market_management_optional_string($input, 'contact_phone') + : null; $stmt = $pdo->prepare( - 'INSERT INTO market_occurrence_organizer (occurrence_id, organization_contact_id, contact_person_id) - VALUES (:occurrence_id, :organization_contact_id, :contact_person_id) + 'INSERT INTO market_occurrence_organizer (occurrence_id, contact_person_name, contact_email, contact_phone) + VALUES (:occurrence_id, :contact_person_name, :contact_email, :contact_phone) ON CONFLICT (occurrence_id) DO UPDATE - SET organization_contact_id = EXCLUDED.organization_contact_id, - contact_person_id = EXCLUDED.contact_person_id, + SET contact_person_name = EXCLUDED.contact_person_name, + contact_email = EXCLUDED.contact_email, + contact_phone = EXCLUDED.contact_phone, updated_at = NOW() RETURNING *' ); $stmt->execute([ ':occurrence_id' => $occurrenceId, - ':organization_contact_id' => $organizationId, - ':contact_person_id' => $contactPersonId, + ':contact_person_name' => $name, + ':contact_email' => $email, + ':contact_phone' => $phone, ]); return (array) $stmt->fetch(); } diff --git a/modules/shared/auth/ui/home.php b/modules/shared/auth/ui/home.php index 698af14..b8afeb6 100644 --- a/modules/shared/auth/ui/home.php +++ b/modules/shared/auth/ui/home.php @@ -164,13 +164,14 @@ function auth_render_market_definitions_content(array $marketTable): string { $rows = $marketTable['rows'] ?? $marketTable; $categories = $marketTable['categories'] ?? []; + $organizers = $marketTable['organizers'] ?? []; $html = []; $html[] = '