+
+
+
+ {columns.map((col) => (
+ | col.sortable && handleSort(col.key)}
+ >
+ {col.label}
+ {col.sortable && sortKey === col.key && (
+
+ {sortDir === 'asc' ? ' ▲' : ' ▼'}
+
+ )}
+ |
+ ))}
+
+
+
+ {pageData.map((row, i) => (
+ onRowClick?.(row)}
+ >
+ {columns.map((col) => (
+ |
+ {String(row[col.key] ?? '')}
+ |
+ ))}
+
+ ))}
+
+
+
+
+ Page {page} of {totalPages}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/cntr/LiveOccupancy/LiveOccupancyWidget.test.tsx b/frontend/cntr/LiveOccupancy/LiveOccupancyWidget.test.tsx
new file mode 100644
index 0000000..5b02cc3
--- /dev/null
+++ b/frontend/cntr/LiveOccupancy/LiveOccupancyWidget.test.tsx
@@ -0,0 +1,43 @@
+import { render, screen } from '@testing-library/react';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { LiveOccupancyWidget } from './LiveOccupancyWidget';
+
+class MockWebSocket {
+ static instances: MockWebSocket[] = [];
+ onopen: (() => void) | null = null;
+ onmessage: ((e: { data: string }) => void) | null = null;
+ onclose: (() => void) | null = null;
+ onerror: (() => void) | null = null;
+ readyState = 0;
+ close = vi.fn();
+ constructor() { MockWebSocket.instances.push(this); }
+}
+
+beforeEach(() => {
+ MockWebSocket.instances = [];
+ (global as any).WebSocket = MockWebSocket;
+});
+
+afterEach(() => { vi.restoreAllMocks(); });
+
+describe('LiveOccupancyWidget', () => {
+ it('shows connecting state initially', () => {
+ render(