Add SSE live updates for bestellungen
This commit is contained in:
@@ -352,3 +352,28 @@ SQL;
|
||||
'next_limit' => min($totalCount, $limit + $pageSize),
|
||||
];
|
||||
}
|
||||
|
||||
function get_sales_order_realtime_snapshot(PDO $pdo, int $limit = 1000): array
|
||||
{
|
||||
$limit = max(1, min(2000, $limit));
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT id, created_at, updated_at
|
||||
FROM public.sales_order
|
||||
ORDER BY updated_at DESC NULLS LAST, id DESC
|
||||
LIMIT :limit'
|
||||
);
|
||||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
$rows = [];
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
$rows[] = [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'created_at' => (string) ($row['created_at'] ?? ''),
|
||||
'updated_at' => (string) ($row['updated_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ function auth_render_bestellungen_large_table(array $bestellungenTable): string
|
||||
$city = (string) ($row['city'] ?? '');
|
||||
$country = (string) ($row['country_name'] ?? '');
|
||||
|
||||
$html[] = '<div class="' . $rowClass . '" role="row" data-component-part="large-table-row" data-bestellungen-row="true">';
|
||||
$html[] = '<div class="' . $rowClass . '" role="row" data-component-part="large-table-row" data-bestellungen-row="true" data-order-id="' . (int) ($row['id'] ?? 0) . '">';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html($orderDate) . '</div>';
|
||||
$html[] = '<div class="sg-large-table__cell" role="cell">';
|
||||
$html[] = '<a class="sg-hyperlink" href="#" aria-haspopup="dialog" aria-expanded="false" data-order-drawer-open="true" data-order-id="' . (int) ($row['id'] ?? 0) . '" data-order-number="' . auth_escape_html($orderNumber) . '" data-order-date="' . auth_escape_html($orderDate) . '" data-order-total="' . auth_escape_html($totalAmount) . '" data-order-first-name="' . auth_escape_html($firstName) . '" data-order-last-name="' . auth_escape_html($lastName) . '" data-order-street="' . auth_escape_html($street) . '" data-order-house-number="' . auth_escape_html($houseNumber) . '" data-order-zip="' . auth_escape_html($zip) . '" data-order-city="' . auth_escape_html($city) . '" data-order-country="' . auth_escape_html($country) . '">' . auth_escape_html($orderNumber) . '</a>';
|
||||
@@ -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]');";
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../../modules/shared/db.php';
|
||||
require_once __DIR__ . '/../../../modules/shared/auth/service.php';
|
||||
require_once __DIR__ . '/../../../modules/erp/bestellungen/service.php';
|
||||
|
||||
header('Content-Type: text/event-stream; charset=utf-8');
|
||||
header('Cache-Control: no-cache, no-transform');
|
||||
header('Connection: keep-alive');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
ignore_user_abort(true);
|
||||
set_time_limit(0);
|
||||
|
||||
if (function_exists('apache_setenv')) {
|
||||
@apache_setenv('no-gzip', '1');
|
||||
}
|
||||
@ini_set('zlib.output_compression', '0');
|
||||
|
||||
$env = expand_env_values(parse_env_file(__DIR__ . '/../../../.env'));
|
||||
|
||||
try {
|
||||
$pdo = connect_database($env);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo 'Database connection failed';
|
||||
exit;
|
||||
}
|
||||
|
||||
auth_bootstrap_session();
|
||||
$currentUser = auth_current_user($pdo);
|
||||
if ($currentUser === null) {
|
||||
http_response_code(401);
|
||||
echo 'Unauthorized';
|
||||
exit;
|
||||
}
|
||||
|
||||
session_write_close();
|
||||
|
||||
while (ob_get_level() > 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user