/** * CLQMS V2 Frontend - Alpine.js Components */ document.addEventListener('alpine:init', () => { /** * Toast Store */ Alpine.store('toast', { messages: [], show(message, type = 'info', duration = 4000) { const id = Date.now(); this.messages.push({ id, message, type }); setTimeout(() => this.dismiss(id), duration); }, dismiss(id) { this.messages = this.messages.filter(m => m.id !== id); }, success(msg) { this.show(msg, 'success'); }, error(msg) { this.show(msg, 'error', 6000); }, info(msg) { this.show(msg, 'info'); } }); /** * API Response Store */ Alpine.store('apiResponse', { hasResponse: false, status: '', statusClass: '', time: 0, body: '', set(status, body, time) { this.hasResponse = true; this.status = status; this.statusClass = status >= 200 && status < 300 ? 'badge-success' : 'badge-error'; this.time = time; this.body = typeof body === 'string' ? body : JSON.stringify(body, null, 2); } }); /** * API Tester Component */ Alpine.data('apiTester', () => ({ method: 'GET', url: '/api/patient', body: '{\n \n}', loading: false, setEndpoint(method, url) { this.method = method; this.url = url; }, async sendRequest() { this.loading = true; const startTime = performance.now(); try { const options = { method: this.method, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, credentials: 'include' }; if (this.method === 'POST' || this.method === 'PATCH') { try { options.body = this.body; JSON.parse(this.body); // Validate JSON } catch (e) { Alpine.store('toast').error('Invalid JSON body'); this.loading = false; return; } } const response = await fetch(this.url, options); const endTime = performance.now(); let data; const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { data = await response.json(); } else { data = await response.text(); } Alpine.store('apiResponse').set( response.status, data, Math.round(endTime - startTime) ); } catch (err) { Alpine.store('toast').error('Request failed: ' + err.message); } finally { this.loading = false; } } })); /** * DB Browser Component */ Alpine.data('dbBrowser', () => ({ tables: [], loadingTables: true, selectedTable: null, tableData: null, loadingData: false, init() { this.loadTables(); }, async loadTables() { this.loadingTables = true; try { const res = await fetch('/v2/api/tables', { credentials: 'include' }); const data = await res.json(); this.tables = data.tables || []; } catch (e) { Alpine.store('toast').error('Failed to load tables'); } finally { this.loadingTables = false; } }, async selectTable(table) { this.selectedTable = table; this.loadingData = true; this.tableData = null; try { const res = await fetch(`/v2/api/table/${table}`, { credentials: 'include' }); this.tableData = await res.json(); } catch (e) { Alpine.store('toast').error('Failed to load table data'); } finally { this.loadingData = false; } } })); /** * Logs Viewer Component */ Alpine.data('logsViewer', () => ({ logs: [], loading: true, expandedLogs: [], init() { this.loadLogs(); }, async loadLogs() { this.loading = true; try { const res = await fetch('/v2/api/logs', { credentials: 'include' }); const data = await res.json(); this.logs = data.logs || []; } catch (e) { Alpine.store('toast').error('Failed to load logs'); } finally { this.loading = false; } }, toggleLog(name) { if (this.expandedLogs.includes(name)) { this.expandedLogs = this.expandedLogs.filter(n => n !== name); } else { this.expandedLogs.push(name); } }, formatSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } })); });