' . auth_escape_html($orderDate) . '
';
$html[] = '
' . auth_escape_html($orderNumber) . '';
@@ -509,6 +509,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " mediaQuery.addEventListener('change', syncMode);";
echo "})();";
echo "let bestellungenTableHandlersInstalled = false;";
+ echo "let bestellungenRealtimeSource = null;";
echo "function initBestellungenBindings() {";
echo " const contentRoot = document.querySelector('[data-left-navigation-content-body]');";
echo " if (!contentRoot) { return; }";
@@ -677,6 +678,74 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " }";
echo " syncSearchState();";
echo " };";
+ echo " const getCurrentQueryParams = () => ({";
+ echo " bestellungen_search: getSearchValue(),";
+ echo " bestellungen_sort: getSortColumn(),";
+ echo " bestellungen_dir: getSortDirection(),";
+ echo " bestellungen_limit: getCurrentLimit(),";
+ echo " });";
+ echo " const getOpenDrawerOrderId = () => {";
+ echo " const trigger = Array.from(contentRoot.querySelectorAll('[data-order-drawer-open]')).find((button) => button.getAttribute('aria-expanded') === 'true');";
+ echo " return trigger ? (trigger.dataset.orderId || '') : '';";
+ echo " };";
+ echo " const reloadBestellungenSection = () => loadFragment(getCurrentQueryParams());";
+ echo " const reloadBestellungenRow = async (orderId) => {";
+ echo " const targetOrderId = String(orderId || '');";
+ echo " if (targetOrderId === '') {";
+ echo " await reloadBestellungenSection();";
+ echo " return;";
+ echo " }";
+ echo " const response = await fetch(buildFragmentUrl(getCurrentQueryParams()).toString(), { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } });";
+ echo " if (!response.ok) {";
+ echo " await reloadBestellungenSection();";
+ echo " return;";
+ echo " }";
+ echo " const html = await response.text();";
+ echo " const doc = new DOMParser().parseFromString(html, 'text/html');";
+ echo " const newRow = Array.from(doc.querySelectorAll('[data-bestellungen-row]')).find((row) => (row.dataset.orderId || '') === targetOrderId) || null;";
+ echo " const currentRow = Array.from(contentRoot.querySelectorAll('[data-bestellungen-row]')).find((row) => (row.dataset.orderId || '') === targetOrderId) || null;";
+ echo " if (!newRow || !currentRow) {";
+ echo " await reloadBestellungenSection();";
+ echo " return;";
+ echo " }";
+ echo " const openDrawerOrderId = getOpenDrawerOrderId();";
+ echo " currentRow.outerHTML = newRow.outerHTML;";
+ echo " if (openDrawerOrderId === targetOrderId) {";
+ echo " const reopenedTrigger = Array.from(contentRoot.querySelectorAll('[data-order-drawer-open]')).find((button) => (button.dataset.orderId || '') === targetOrderId) || null;";
+ echo " if (reopenedTrigger) {";
+ echo " openDrawer(reopenedTrigger);";
+ echo " }";
+ echo " }";
+ echo " };";
+ echo " const connectBestellungenRealtime = () => {";
+ echo " if (!window.EventSource) { return null; }";
+ echo " if (bestellungenRealtimeSource) {";
+ echo " bestellungenRealtimeSource.close();";
+ echo " }";
+ echo " const source = new EventSource('/api/realtime/bestellungen.php', { withCredentials: true });";
+ echo " source.addEventListener('bestellungen.changed', (event) => {";
+ echo " const payload = parseJsonRealtimeEvent(event.data);";
+ echo " if (!payload) { return; }";
+ echo " const kind = String(payload.kind || '');";
+ echo " const orderId = payload.orderId !== undefined && payload.orderId !== null ? String(payload.orderId) : '';";
+ echo " if (kind === 'updated' && orderId !== '') {";
+ echo " void reloadBestellungenRow(orderId);";
+ echo " return;";
+ echo " }";
+ echo " void reloadBestellungenSection();";
+ echo " });";
+ echo " bestellungenRealtimeSource = source;";
+ echo " return source;";
+ echo " };";
+ echo " const parseJsonRealtimeEvent = (raw) => {";
+ echo " if (typeof raw !== 'string' || raw.trim() === '') { return null; }";
+ echo " try {";
+ echo " const parsed = JSON.parse(raw);";
+ echo " return parsed && typeof parsed === 'object' ? parsed : null;";
+ echo " } catch (error) {";
+ echo " return null;";
+ echo " }";
+ echo " };";
echo " if (!bestellungenTableHandlersInstalled) {";
echo " bestellungenTableHandlersInstalled = true;";
echo " contentRoot.addEventListener('click', (event) => {";
@@ -727,6 +796,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " });";
echo " }";
echo " bindTable();";
+ echo " connectBestellungenRealtime();";
echo "}";
echo "(() => {";
echo " const overlay = document.querySelector('[data-otc-order-overlay]');";
diff --git a/public/api/realtime/bestellungen.php b/public/api/realtime/bestellungen.php
new file mode 100644
index 0000000..ae52247
--- /dev/null
+++ b/public/api/realtime/bestellungen.php
@@ -0,0 +1,115 @@
+ 0) {
+ ob_end_flush();
+}
+
+$sendEvent = static function (string $eventName, array $payload): void {
+ echo 'event: ' . $eventName . "\n";
+ echo 'data: ' . json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n\n";
+ flush();
+};
+
+$lastHeartbeatAt = time();
+$state = [];
+foreach (get_sales_order_realtime_snapshot($pdo) as $row) {
+ $state[(int) $row['id']] = (string) $row['updated_at'];
+}
+
+echo ": connected\n\n";
+flush();
+
+while (!connection_aborted()) {
+ try {
+ $snapshot = get_sales_order_realtime_snapshot($pdo);
+ $nextState = [];
+
+ foreach ($snapshot as $row) {
+ $orderId = (int) ($row['id'] ?? 0);
+ if ($orderId <= 0) {
+ continue;
+ }
+
+ $updatedAt = (string) ($row['updated_at'] ?? '');
+ $nextState[$orderId] = $updatedAt;
+
+ if (!array_key_exists($orderId, $state)) {
+ $sendEvent('bestellungen.changed', [
+ 'kind' => 'created',
+ 'orderId' => $orderId,
+ 'updatedAt' => $updatedAt,
+ ]);
+ continue;
+ }
+
+ if ($state[$orderId] !== $updatedAt) {
+ $sendEvent('bestellungen.changed', [
+ 'kind' => 'updated',
+ 'orderId' => $orderId,
+ 'updatedAt' => $updatedAt,
+ ]);
+ }
+ }
+
+ $state = $nextState;
+
+ if ((time() - $lastHeartbeatAt) >= 15) {
+ echo ": ping\n\n";
+ flush();
+ $lastHeartbeatAt = time();
+ }
+
+ usleep(2000000);
+ } catch (Throwable $e) {
+ echo ": error\n\n";
+ flush();
+ try {
+ $pdo = connect_database($env);
+ $state = [];
+ foreach (get_sales_order_realtime_snapshot($pdo) as $row) {
+ $state[(int) $row['id']] = (string) $row['updated_at'];
+ }
+ } catch (Throwable $reconnectError) {
+ // Keep the connection open and try again on the next loop.
+ }
+ usleep(5000000);
+ }
+}