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 '
Bestellung erfolgreich erfasst! Die Bestellnummer wird automatisch generiert.
';
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;
}