diff --git a/modules/erp/kontakte/service.php b/modules/erp/kontakte/service.php
index 7eb2795..61ad753 100644
--- a/modules/erp/kontakte/service.php
+++ b/modules/erp/kontakte/service.php
@@ -125,6 +125,32 @@ function get_manufacturer_options(PDO $pdo): array
);
}
+function get_party_reference(PDO $pdo, int $partyId): ?array
+{
+ $stmt = $pdo->prepare(
+ 'SELECT id, name, email, phone
+ FROM party
+ WHERE id = :id'
+ );
+ $stmt->execute([':id' => $partyId]);
+ $party = $stmt->fetch();
+
+ return is_array($party) ? $party : null;
+}
+
+function get_contact_reference(PDO $pdo, int $contactId): ?array
+{
+ $stmt = $pdo->prepare(
+ 'SELECT id, party_id, first_name, last_name, email, phone, position_title
+ FROM contact
+ WHERE id = :id'
+ );
+ $stmt->execute([':id' => $contactId]);
+ $contact = $stmt->fetch();
+
+ return is_array($contact) ? $contact : null;
+}
+
function create_manufacturer(PDO $pdo, string $name, ?string $email = null, ?string $phone = null): array
{
$name = trim($name);
diff --git a/modules/market_management/service.php b/modules/market_management/service.php
index 4187fa7..dbac7e2 100644
--- a/modules/market_management/service.php
+++ b/modules/market_management/service.php
@@ -1,6 +1,8 @@
fetchAll();
$organizer = $pdo->prepare('SELECT * FROM market_occurrence_organizer WHERE occurrence_id = :occurrence_id');
$organizer->execute([':occurrence_id' => $occurrenceId]);
- $occurrence['organizer'] = $organizer->fetch() ?: null;
+ $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']);
+ }
+ $occurrence['organizer'] = is_array($organizer) ? $organizer : null;
$costs = $pdo->prepare('SELECT * FROM market_occurrence_cost_item WHERE occurrence_id = :occurrence_id ORDER BY id ASC');
$costs->execute([':occurrence_id' => $occurrenceId]);
$occurrence['cost_items'] = $costs->fetchAll();
+ $costSummary = $pdo->prepare(
+ 'SELECT COALESCE(SUM(gross_amount), 0) AS gross_amount,
+ COALESCE(SUM(net_amount), 0) AS net_amount,
+ COALESCE(SUM(tax_amount), 0) AS tax_amount
+ FROM market_occurrence_cost_item
+ WHERE occurrence_id = :occurrence_id'
+ );
+ $costSummary->execute([':occurrence_id' => $occurrenceId]);
+ $summary = $costSummary->fetch() ?: ['gross_amount' => 0, 'net_amount' => 0, 'tax_amount' => 0];
+ $occurrence['cost_summary'] = [
+ 'gross_amount' => (float) ($summary['gross_amount'] ?? 0),
+ 'net_amount' => (float) ($summary['net_amount'] ?? 0),
+ 'tax_amount' => (float) ($summary['tax_amount'] ?? 0),
+ 'open_amount' => $occurrence['paid_amount'] === null
+ ? null
+ : round((float) $summary['gross_amount'] - (float) $occurrence['paid_amount'], 2),
+ ];
return $occurrence;
}
diff --git a/modules/shared/auth/ui/home.php b/modules/shared/auth/ui/home.php
index 66dc3af..756a110 100644
--- a/modules/shared/auth/ui/home.php
+++ b/modules/shared/auth/ui/home.php
@@ -1036,13 +1036,17 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " const getPageSize = () => parseInt(getTable()?.dataset.marketsPageSize || '20', 10);";
echo " const marketEscape = (value) => String(value ?? '').replace(/[&<>\"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[character] || character));";
echo " const marketDisplay = (value) => value === null || value === undefined || value === '' ? 'Nicht hinterlegt' : marketEscape(value);";
+ echo " const marketBoolean = (value) => value === true || value === 'true' || value === 1 || value === '1' ? 'Ja' : value === false || value === 'false' || value === 0 || value === '0' ? 'Nein' : 'Nicht hinterlegt';";
+ echo " const marketMoney = (value) => value === null || value === undefined || value === '' ? 'Nicht hinterlegt' : 'CHF ' + Number(value).toFixed(2);";
+ echo " const marketContactName = (contact) => { if (!contact) { return 'Nicht hinterlegt'; } return marketDisplay([contact.first_name, contact.last_name].filter(Boolean).join(' ')); };";
+ echo " const marketCostItems = (items) => Array.isArray(items) && items.length ? '
' + items.map((item) => '- ' + marketDisplay([item.cost_type, item.description].filter(Boolean).join(' – ')) + ': ' + marketMoney(item.gross_amount) + ' brutto, ' + marketMoney(item.net_amount) + ' netto, ' + marketMoney(item.tax_amount) + ' Steuer
').join('') + '
' : 'Keine Kostenpositionen';";
echo " const marketDate = (value) => { if (!value) { return 'Nicht hinterlegt'; } const parts = String(value).split('-'); return parts.length === 3 ? parts[2] + '.' + parts[1] + '.' + parts[0] : marketEscape(value); };";
echo " const marketStatusLabel = (value) => ({ open: 'Offen', waitlist: 'Warteliste', registered: 'Angemeldet', confirmation_pending: 'Bestätigung ausstehend', confirmed: 'Bestätigt', cancelled: 'Abgesagt', completed: 'Abgeschlossen' }[value] || value || 'Nicht hinterlegt');";
echo " const marketStatusSegment = (value) => ({ waitlist: 'sg-card-segment--signal-red', registered: 'sg-card-segment--signal-yellow', confirmation_pending: 'sg-card-segment--signal-yellow', confirmed: 'sg-card-segment--signal-green' }[value] || 'sg-card-segment--darkblue');";
echo " const marketRow = (label, value, specific = false) => '' + marketEscape(label) + (specific ? ' (Tagesspezifisch)' : '') + '' + marketDisplay(value) + '';";
echo " const marketCard = (title, body, headerClass = 'sg-card-segment--darkgreen', actions = '') => '' + title + '
' + actions + '' + body + '
';";
echo " const marketEditButton = (scope, id) => '';";
- echo " const renderMarketDetail = (payload) => { const market = payload.market || {}; const occurrence = payload.occurrence || {}; const days = Array.isArray(occurrence.days) ? occurrence.days : []; const website = market.website_url ? '' + marketEscape(market.website_url) + '' : 'Nicht hinterlegt'; const overview = marketRow('Veranstaltung', 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); const dates = days.map((day) => marketDate(day.date)).join(' – '); const participation = marketRow('Anmeldung / Anfrage', occurrence.request_or_registration_date) + marketRow('Anmeldefrist', occurrence.registration_deadline) + marketRow('Bestätigung', occurrence.confirmation_date) + marketRow('Zahlungsfrist', occurrence.payment_deadline) + marketRow('Zahlungsstatus', occurrence.payment_status); const infrastructure = marketRow('Standgrösse', occurrence.stand_size) + marketRow('Tisch vorhanden', occurrence.table_available) + marketRow('Tisch mietbar', occurrence.table_rentable) + marketRow('Eigener Tisch erforderlich', occurrence.own_table_required) + marketRow('Überdachung', occurrence.shelter_available) + marketRow('Strom', occurrence.power_available) + marketRow('Wasser', occurrence.water_available) + marketRow('Beleuchtung', occurrence.lighting_available); const travel = marketRow('Reiseadresse', occurrence.travel_address) + marketRow('Distanz (km)', occurrence.travel_distance_km) + marketRow('Reisedauer (Minuten)', occurrence.travel_duration_minutes) + marketRow('Parkmöglichkeiten', occurrence.parking_options) + marketRow('Anlieferung', occurrence.delivery_access) + marketRow('Direktes Entladen', occurrence.direct_unload_possible) + marketRow('Lagerung', occurrence.storage_options); const notes = marketRow('Teilnahmebedingungen', occurrence.other_participation_conditions) + marketRow('Rücktrittsbedingungen', occurrence.cancellation_terms) + marketRow('Umsatzbeteiligung', occurrence.revenue_share_terms) + marketRow('Notizen', occurrence.notes); const dayTabs = days.map((day, index) => '').join(''); const dayPanels = days.map((day, index) => '' + marketRow('Datum', marketDate(day.date), 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('Beobachtungen', day.observations, true) + '
').join(''); return '' + marketRow('Datumsbereich', dates) + marketRow('Anzahl Tage', days.length) + '
' + marketCard('Veranstaltung und Durchführung', '' + overview + '
', 'sg-card-segment--darkgreen') + marketCard('Teilnahme und Zahlung', '' + participation + '
', 'sg-card-segment--darkgreen') + marketCard('Infrastruktur', '' + infrastructure + '
', 'sg-card-segment--darkgreen') + marketCard('Anreise und Logistik', '' + travel + '
', 'sg-card-segment--darkgreen') + marketCard('Weitere Angaben', '' + notes + '
', 'sg-card-segment--darkgreen') + (days.length ? '' + dayTabs + '
' + dayPanels + '
' : 'Keine Durchführungstage vorhanden.
'); };";
+ echo " const renderMarketDetail = (payload) => { const market = payload.market || {}; const occurrence = payload.occurrence || {}; const days = Array.isArray(occurrence.days) ? occurrence.days : []; 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('Veranstaltung', 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)).join(' – '); const costSummary = occurrence.cost_summary || {}; const participation = marketRow('Anmeldung / Anfrage', occurrence.request_or_registration_date) + 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' + marketCostItems(occurrence.cost_items) + ''; 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('Reisedauer (Minuten)', occurrence.travel_duration_minutes) + 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) => '').join(''); const dayPanels = days.map((day, index) => '' + marketRow('Datum', marketDate(day.date), 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) + marketRow('Beobachtungen', day.observations, true) + '
').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 '' + marketRow('Datumsbereich', dates) + marketRow('Anzahl Tage', days.length) + '
' + salesCard + marketCard('Veranstaltung und Durchführung', '' + overview + '
', 'sg-card-segment--darkgreen') + marketCard('Teilnahme und Zahlung', '' + participation + '
', 'sg-card-segment--darkgreen') + marketCard('Infrastruktur', '' + infrastructure + '
', 'sg-card-segment--darkgreen') + marketCard('Anreise und Logistik', '' + travel + '
', 'sg-card-segment--darkgreen') + marketCard('Weitere Angaben', '' + notes + '
', 'sg-card-segment--darkgreen') + (days.length ? '' + dayTabs + '
' + dayPanels + '
' : 'Keine Durchführungstage vorhanden.
'); };";
echo " const marketEditForm = (scope, id, values) => { const fields = scope === 'day' ? [['date', 'Datum', 'date'], ['stand_start_time', 'Standzeit von', 'time'], ['stand_end_time', 'Standzeit bis', 'time'], ['setup_start_time', 'Aufbau von', 'time'], ['setup_end_time', 'Aufbau bis', 'time'], ['delivery_start_time', 'Anlieferung von', 'time'], ['delivery_end_time', 'Anlieferung bis', 'time'], ['observations', 'Beobachtungen', 'text']] : [['place', 'Ort', 'text'], ['address', 'Adresse', 'text'], ['registration_deadline', 'Anmeldefrist', 'date'], ['request_or_registration_date', 'Anmeldung / Anfrage', 'date'], ['confirmation_date', 'Bestätigung', 'date'], ['payment_deadline', 'Zahlungsfrist', 'date'], ['notes', 'Notizen', 'text']]; return ''; };";
echo " const buildUrl = (params) => { const url = new URL(window.location.href); Object.entries(params).forEach(([key, value]) => { if (value === null || value === undefined || value === '') { url.searchParams.delete(key); } else { url.searchParams.set(key, String(value)); } }); return url; };";
echo " const replaceFragment = (html) => { const doc = new DOMParser().parseFromString(html, 'text/html'); const fragment = doc.querySelector('[data-markets-content]'); if (!fragment) { return false; } contentRoot.innerHTML = fragment.innerHTML; return Boolean(getTable()); };";
diff --git a/public/api/market-occurrence.php b/public/api/market-occurrence.php
index 3eb3ba8..ed27d7b 100644
--- a/public/api/market-occurrence.php
+++ b/public/api/market-occurrence.php
@@ -3,6 +3,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../../modules/shared/db.php';
require_once __DIR__ . '/../../modules/shared/auth/service.php';
+require_once __DIR__ . '/../../modules/erp/kontakte/service.php';
require_once __DIR__ . '/../../modules/market_management/service.php';
header('Content-Type: application/json; charset=utf-8');