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 ? '' : '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) => '
' + marketDate(day.date) + '
' + marketEditButton('day', day.id) + '
' + 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 '
Status: ' + marketEscape(marketStatusLabel(occurrence.status)) + '
' + marketEditButton('occurrence', occurrence.id) + '
' + 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) => '
' + marketDate(day.date) + '
' + marketEditButton('day', day.id) + '
' + 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 '
Status: ' + marketEscape(marketStatusLabel(occurrence.status)) + '
' + marketEditButton('occurrence', occurrence.id) + '
' + 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 '
' + fields.map(([key, label, type]) => '
' + marketEscape(label) + '
').join('') + '
'; };"; 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');