From 47c09a3e4c966119fd155d21cb277de681cc1215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Gla=CC=88ser?= Date: Sat, 19 Sep 2026 17:32:23 +0200 Subject: [PATCH] Link OTC sales to market days --- db/migrations/0027_otc_market_link.sql | 28 ++++++++ modules/erp/bestellungen/service.php | 8 ++- modules/erp/direktverkauf/api/otc-order.php | 48 +++++++++++++- modules/market_management/service.php | 16 +++++ modules/shared/auth/ui/home.php | 73 +++++++++++++++++++-- public/index.php | 3 +- 6 files changed, 167 insertions(+), 9 deletions(-) create mode 100644 db/migrations/0027_otc_market_link.sql diff --git a/db/migrations/0027_otc_market_link.sql b/db/migrations/0027_otc_market_link.sql new file mode 100644 index 0000000..9db184a --- /dev/null +++ b/db/migrations/0027_otc_market_link.sql @@ -0,0 +1,28 @@ +BEGIN; + +ALTER TABLE sales_order + ADD COLUMN market_occurrence_id BIGINT, + ADD COLUMN market_occurrence_day_id BIGINT; + +ALTER TABLE market_occurrence_day + ADD CONSTRAINT uq_market_occurrence_day_occurrence_id UNIQUE (occurrence_id, id); + +ALTER TABLE sales_order + ADD CONSTRAINT fk_sales_order_market_occurrence + FOREIGN KEY (market_occurrence_id) + REFERENCES market_occurrence (id) + ON DELETE RESTRICT, + ADD CONSTRAINT fk_sales_order_market_occurrence_day + FOREIGN KEY (market_occurrence_id, market_occurrence_day_id) + REFERENCES market_occurrence_day (occurrence_id, id) + ON DELETE RESTRICT, + ADD CONSTRAINT chk_sales_order_market_link_complete + CHECK ((market_occurrence_id IS NULL) = (market_occurrence_day_id IS NULL)); + +CREATE INDEX idx_sales_order_market_occurrence + ON sales_order (market_occurrence_id); + +CREATE INDEX idx_sales_order_market_occurrence_day + ON sales_order (market_occurrence_day_id); + +COMMIT; diff --git a/modules/erp/bestellungen/service.php b/modules/erp/bestellungen/service.php index 51463a9..091dfef 100644 --- a/modules/erp/bestellungen/service.php +++ b/modules/erp/bestellungen/service.php @@ -146,10 +146,12 @@ function create_direct_sales_order(PDO $pdo, array $fields): array $stmt = $pdo->prepare( 'INSERT INTO sales_order ( id, external_ref, party_id, order_source, order_date, order_status, payment_status, payment_method_id, - amount_net, amount_shipping, amount_tax, amount_discount, total_amount, currency, imported_at, created_at, updated_at + amount_net, amount_shipping, amount_tax, amount_discount, total_amount, currency, + market_occurrence_id, market_occurrence_day_id, imported_at, created_at, updated_at ) VALUES ( :id, :external_ref, :party_id, \'direct\', :order_date, \'imported\', \'paid\', :payment_method_id, - :amount_net, 0, 0, 0, :total_amount, \'CHF\', NOW(), NOW(), NOW() + :amount_net, 0, 0, 0, :total_amount, \'CHF\', + :market_occurrence_id, :market_occurrence_day_id, NOW(), NOW(), NOW() ) RETURNING id, external_ref' ); @@ -161,6 +163,8 @@ function create_direct_sales_order(PDO $pdo, array $fields): array ':payment_method_id' => $fields[':payment_method_id'] ?? null, ':amount_net' => $fields[':amount_net'] ?? null, ':total_amount' => $fields[':total_amount'] ?? null, + ':market_occurrence_id' => $fields[':market_occurrence_id'] ?? null, + ':market_occurrence_day_id' => $fields[':market_occurrence_day_id'] ?? null, ]); $row = $stmt->fetch(); diff --git a/modules/erp/direktverkauf/api/otc-order.php b/modules/erp/direktverkauf/api/otc-order.php index f323943..8206746 100644 --- a/modules/erp/direktverkauf/api/otc-order.php +++ b/modules/erp/direktverkauf/api/otc-order.php @@ -41,6 +41,20 @@ $products = $data['products']; $orderDate = trim((string) $data['orderDate']); $payments = $data['payments']; $billing = is_array($data['billing']) ? $data['billing'] : []; +$marketOccurrenceId = $data['marketOccurrenceId'] ?? null; +$marketOccurrenceDayId = $data['marketOccurrenceDayId'] ?? null; + +if (($marketOccurrenceId === null) !== ($marketOccurrenceDayId === null)) { + json_response(422, ['ok' => false, 'error' => 'Veranstaltung und Durchführungstag müssen gemeinsam angegeben werden.']); +} + +if ($marketOccurrenceId !== null && (!ctype_digit((string) $marketOccurrenceId) || (int) $marketOccurrenceId <= 0)) { + json_response(422, ['ok' => false, 'error' => 'Ungültige Veranstaltung.']); +} + +if ($marketOccurrenceDayId !== null && (!ctype_digit((string) $marketOccurrenceDayId) || (int) $marketOccurrenceDayId <= 0)) { + json_response(422, ['ok' => false, 'error' => 'Ungültiger Durchführungstag.']); +} if (!is_array($products) || count($products) === 0) { json_response(422, ['ok' => false, 'error' => 'No products specified']); @@ -115,6 +129,27 @@ try { $pdo = connect_database($env); $pdo->beginTransaction(); + if ($marketOccurrenceId !== null) { + $marketLinkStmt = $pdo->prepare( + 'SELECT 1 + FROM market_occurrence o + INNER JOIN market_definition m ON m.id = o.market_definition_id + INNER JOIN market_occurrence_day d ON d.occurrence_id = o.id + WHERE o.id = :occurrence_id + AND d.id = :day_id + AND m.archived_at IS NULL + AND o.archived_at IS NULL + LIMIT 1' + ); + $marketLinkStmt->execute([ + ':occurrence_id' => (int) $marketOccurrenceId, + ':day_id' => (int) $marketOccurrenceDayId, + ]); + if ($marketLinkStmt->fetchColumn() === false) { + throw new InvalidArgumentException('Die Veranstaltung oder der Durchführungstag ist nicht aktiv oder nicht miteinander verknüpft.'); + } + } + $locations = get_default_location_ids($pdo); $paymentMethodIds = []; @@ -135,6 +170,8 @@ try { ':amount_net' => $totalPrice, ':total_amount' => $totalPrice, ':order_date' => $orderDateTime->format('Y-m-d H:i:s'), + ':market_occurrence_id' => $marketOccurrenceId === null ? null : (int) $marketOccurrenceId, + ':market_occurrence_day_id' => $marketOccurrenceDayId === null ? null : (int) $marketOccurrenceDayId, ]); $orderId = (int) $order['id']; @@ -221,7 +258,16 @@ try { 'excelTrigger' => $excelTrigger, 'message' => 'OTC order created successfully', ]); -} catch (Throwable $e) { + } catch (InvalidArgumentException $e) { + if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) { + $pdo->rollBack(); + } + + json_response(422, [ + 'ok' => false, + 'error' => $e->getMessage(), + ]); + } catch (Throwable $e) { if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) { $pdo->rollBack(); } diff --git a/modules/market_management/service.php b/modules/market_management/service.php index 68c5dc0..29fd303 100644 --- a/modules/market_management/service.php +++ b/modules/market_management/service.php @@ -342,6 +342,22 @@ function market_management_search_occurrences(PDO $pdo, array $input): array return $stmt->fetchAll(); } +function market_management_list_active_occurrence_options(PDO $pdo): array +{ + $stmt = $pdo->query( + 'SELECT o.id AS occurrence_id, m.name AS market_name, o.status, + d.id AS day_id, d.date + FROM market_occurrence o + INNER JOIN market_definition m ON m.id = o.market_definition_id + INNER JOIN market_occurrence_day d ON d.occurrence_id = o.id + WHERE m.archived_at IS NULL + AND o.archived_at IS NULL + ORDER BY m.name ASC, o.id ASC, d.date ASC, d.id ASC' + ); + + return $stmt->fetchAll(); +} + function market_management_list_reference_data(PDO $pdo, array $input): array { $result = ['categories' => market_management_list_categories($pdo)]; diff --git a/modules/shared/auth/ui/home.php b/modules/shared/auth/ui/home.php index 8c45d4a..068c080 100644 --- a/modules/shared/auth/ui/home.php +++ b/modules/shared/auth/ui/home.php @@ -595,7 +595,7 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar . ''; } -function render_auth_home_page(array $user, array $otcProducts = [], array $bestellungenTable = [], array $marketOccurrencesTable = [], array $lagerProducts = [], array $lagerArtikel = [], array $lagerManufacturers = []): void +function render_auth_home_page(array $user, array $otcProducts = [], array $otcMarketOptions = [], array $bestellungenTable = [], array $marketOccurrencesTable = [], array $lagerProducts = [], array $lagerArtikel = [], array $lagerManufacturers = []): void { $otcProductRows = ''; foreach ($otcProducts as $product) { @@ -615,6 +615,25 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best $otcProductRows .= ''; } + $otcMarketOccurrenceOptions = []; + $otcMarketDayOptions = ''; + foreach ($otcMarketOptions as $option) { + $occurrenceId = (int) ($option['occurrence_id'] ?? 0); + $dayId = (int) ($option['day_id'] ?? 0); + if ($occurrenceId <= 0 || $dayId <= 0) { + continue; + } + $marketName = (string) ($option['market_name'] ?? ''); + $otcMarketOccurrenceOptions[$occurrenceId] = $marketName; + $day = (string) ($option['date'] ?? ''); + $dayLabel = $day === '' ? '' : (new DateTimeImmutable($day))->format('d.m.Y'); + $otcMarketDayOptions .= ''; + } + $otcMarketOccurrenceOptionHtml = ''; + foreach ($otcMarketOccurrenceOptions as $occurrenceId => $marketName) { + $otcMarketOccurrenceOptionHtml .= ''; + } + if ($otcProductRows === '') { $otcProductRows = '

Keine aktiven OTC-Produkte erfasst.

'; } @@ -792,6 +811,31 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best echo ''; echo ''; echo ''; + echo '
'; + echo ''; + echo '
'; + echo ''; + echo '
'; + echo '
'; + echo ''; + echo '
'; + echo '
'; + echo ''; + echo '
'; + echo '
'; echo '
'; echo ''; echo ''; @@ -1427,6 +1471,10 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best echo " const houseNumberInput = form.querySelector('[data-otc-order-house-number]');"; echo " const zipInput = form.querySelector('[data-otc-order-zip]');"; echo " const cityInput = form.querySelector('[data-otc-order-city]');"; + echo " const marketLinkToggle = form.querySelector('[data-otc-market-link-toggle]');"; + echo " const marketLinkEnabledInput = form.querySelector('[data-otc-market-link-enabled]');"; + echo " const marketOccurrenceInput = form.querySelector('[data-otc-market-occurrence]');"; + echo " const marketDayInput = form.querySelector('[data-otc-market-day]');"; echo " const submitBtn = overlay.querySelector('[data-otc-order-submit]');"; echo " const errorEl = form.querySelector('[data-otc-order-error]');"; echo " const successEl = form.querySelector('[data-otc-order-success]');"; @@ -1480,6 +1528,10 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best echo " };"; echo " const getInputValue = (input) => parseInt(input.value || '0', 10) || 0;"; echo " const getFieldValue = (input) => (input ? input.value.trim() : '');"; + echo " const isMarketLinkEnabled = () => marketLinkToggle?.getAttribute('aria-checked') === 'true';"; + echo " const setDisabledSelectState = (select, disabled) => { if (!select) { return; } select.disabled = disabled; select.setAttribute('aria-disabled', String(disabled)); select.classList.toggle('sg-form-disabled', disabled); select.classList.toggle('sg-input-single-line--disabled', disabled); select.classList.toggle('sg-form-inactive-selectable', !disabled); select.classList.toggle('sg-input-single-line--inactive-selectable', !disabled); };"; + echo " const syncMarketDayOptions = () => { if (!marketOccurrenceInput || !marketDayInput) { return; } const occurrenceId = marketOccurrenceInput.value; Array.from(marketDayInput.options).forEach((option, index) => { if (index === 0) { option.hidden = false; option.disabled = false; return; } const matches = occurrenceId !== '' && option.dataset.marketOccurrenceId === occurrenceId; option.hidden = !matches; option.disabled = !matches; }); if (!occurrenceId || !Array.from(marketDayInput.options).some((option) => option.value === marketDayInput.value && !option.disabled)) { marketDayInput.value = ''; } setDisabledSelectState(marketDayInput, !isMarketLinkEnabled() || occurrenceId === ''); };"; + echo " const syncMarketLinkState = () => { const enabled = isMarketLinkEnabled(); if (marketLinkEnabledInput) { marketLinkEnabledInput.value = enabled ? '1' : '0'; } setDisabledSelectState(marketOccurrenceInput, !enabled); if (!enabled && marketOccurrenceInput) { marketOccurrenceInput.value = ''; } syncMarketDayOptions(); };"; echo " const updatePaymentAddButton = () => { addPaymentButton.disabled = paymentRows.querySelectorAll('[data-otc-payment-row]').length >= paymentOptionValues.length; };"; echo " const updateFormState = () => {"; echo " const totalQty = productInputs.reduce((sum, input) => sum + getInputValue(input), 0);"; @@ -1491,7 +1543,8 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best echo " && !!getFieldValue(streetInput)"; echo " && !!getFieldValue(houseNumberInput)"; echo " && !!getFieldValue(zipInput)"; - echo " && !!getFieldValue(cityInput);"; + echo " && !!getFieldValue(cityInput)"; + echo " && (!isMarketLinkEnabled() || (!!getFieldValue(marketOccurrenceInput) && !!getFieldValue(marketDayInput)));"; echo " let errorMsg = '';"; echo " if (totalQty === 0) {"; echo " errorMsg = 'Mindestens ein Produkt mit Menge > 0 erforderlich.';"; @@ -1499,6 +1552,8 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best echo " errorMsg = 'Bestelldatum auswählen.';"; echo " } else if (!getFieldValue(firstNameInput) || !getFieldValue(lastNameInput) || !getFieldValue(streetInput) || !getFieldValue(houseNumberInput) || !getFieldValue(zipInput) || !getFieldValue(cityInput)) {"; echo " errorMsg = 'Alle Rechnungsadress-Felder ausfüllen.';"; + echo " } else if (isMarketLinkEnabled() && (!getFieldValue(marketOccurrenceInput) || !getFieldValue(marketDayInput))) {"; + echo " errorMsg = 'Veranstaltung und Durchführungstag auswählen.';"; echo " }"; echo " if (errorEl) {"; echo " errorEl.textContent = errorMsg;"; @@ -1515,6 +1570,8 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best echo " updatePaymentAddButton();"; echo " clearError();"; echo " if (!preserveSuccess) { clearSuccess(); }"; + echo " if (marketLinkToggle) { marketLinkToggle.setAttribute('aria-checked', 'false'); marketLinkToggle.classList.remove('sg-form-active'); marketLinkToggle.classList.add('sg-checkbox-field--inactive-selectable'); }"; + echo " syncMarketLinkState();"; echo " if (!preserveSuccess) { updateFormState(); }"; echo " };"; echo " const closeConfirmation = () => {"; @@ -1581,6 +1638,9 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best echo " addPaymentButton.addEventListener('click', addPaymentRow);"; echo " bindPaymentRow(paymentRows.querySelector('[data-otc-payment-row]'));"; echo " updatePaymentAddButton();"; + echo " marketLinkToggle?.addEventListener('click', (event) => { event.stopImmediatePropagation(); const enabled = marketLinkToggle.getAttribute('aria-checked') !== 'true'; marketLinkToggle.setAttribute('aria-checked', String(enabled)); marketLinkToggle.classList.toggle('sg-form-active', enabled); marketLinkToggle.classList.toggle('sg-checkbox-field--inactive-selectable', !enabled); syncMarketLinkState(); updateFormState(); });"; + echo " marketOccurrenceInput?.addEventListener('change', () => { syncMarketDayOptions(); updateFormState(); });"; + echo " marketDayInput?.addEventListener('change', updateFormState);"; echo " const submitOrder = async (orderData) => {"; echo " let submissionSucceeded = false;"; echo " const originalBtnText = confirmationSubmit.innerHTML;"; @@ -1630,14 +1690,16 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best echo " products: products,"; echo " orderDate: orderDateInput ? orderDateInput.value : '',"; echo " payments: Array.from(paymentRows.querySelectorAll('[data-otc-payment-row]')).map((row) => ({ paymentMethod: row.querySelector('[data-otc-payment-method]').value, amount: parseFloat(row.querySelector('[data-otc-payment-amount]').value || '0') || 0 })),"; - echo " billing: {"; + echo " billing: {"; echo " firstName: getFieldValue(firstNameInput),"; echo " lastName: getFieldValue(lastNameInput),"; echo " street: getFieldValue(streetInput),"; echo " houseNumber: getFieldValue(houseNumberInput),"; echo " zip: getFieldValue(zipInput),"; - echo " city: getFieldValue(cityInput)"; - echo " }"; + echo " city: getFieldValue(cityInput)"; + echo " },"; + echo " marketOccurrenceId: isMarketLinkEnabled() ? getFieldValue(marketOccurrenceInput) : null,"; + echo " marketOccurrenceDayId: isMarketLinkEnabled() ? getFieldValue(marketDayInput) : null"; echo " };"; echo " openConfirmation(orderData);"; echo " });"; @@ -1645,6 +1707,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best echo " if (event.target.closest('[data-otc-confirmation-cancel]')) { closeConfirmation(); return; }"; echo " if (event.target.closest('[data-otc-confirmation-submit]') && pendingOrderData) { submitOrder(pendingOrderData); }"; echo " });"; + echo " syncMarketLinkState();"; echo " updateFormState();"; echo "})();"; echo "if (window.sgInitHelpIconOverlays) {"; diff --git a/public/index.php b/public/index.php index ebdc263..c013dc0 100644 --- a/public/index.php +++ b/public/index.php @@ -49,6 +49,7 @@ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') { $currentUser = auth_current_user($pdo); if ($currentUser !== null) { $otcProducts = get_otc_order_form_products($pdo); + $otcMarketOptions = market_management_list_active_occurrence_options($pdo); $lagerProducts = get_lager_product_overview($pdo); $lagerManufacturers = get_manufacturer_options($pdo); $lagerArtikel = get_lager_artikel_overview($pdo); @@ -86,7 +87,7 @@ if ($currentUser !== null) { echo auth_render_markets_content($marketOccurrencesTable); exit; } - render_auth_home_page($currentUser, $otcProducts, $bestellungenTable, $marketOccurrencesTable, $lagerProducts, $lagerArtikel, $lagerManufacturers); + render_auth_home_page($currentUser, $otcProducts, $otcMarketOptions, $bestellungenTable, $marketOccurrencesTable, $lagerProducts, $lagerArtikel, $lagerManufacturers); exit; }