feat: add operational database schema and live queue walk-in flow
Build Curio HMS / build-and-deploy (push) Successful in 57s
Details
Build Curio HMS / build-and-deploy (push) Successful in 57s
Details
This commit is contained in:
parent
ba88e9094d
commit
2ef518f30e
|
|
@ -97,6 +97,32 @@ CREATE TABLE IF NOT EXISTS prescriptions (
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS patients (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
tenant_id TEXT REFERENCES tenants(id) ON DELETE CASCADE,
|
||||||
|
display_id TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
whatsapp_number TEXT NOT NULL,
|
||||||
|
age INTEGER,
|
||||||
|
gender TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
UNIQUE(tenant_id, whatsapp_number)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS visits (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
tenant_id TEXT REFERENCES tenants(id) ON DELETE CASCADE,
|
||||||
|
patient_id INTEGER REFERENCES patients(id) ON DELETE CASCADE,
|
||||||
|
token_number TEXT NOT NULL,
|
||||||
|
status TEXT DEFAULT 'waiting' CHECK (status IN ('waiting', 'in-room', 'completed', 'cancelled')),
|
||||||
|
type TEXT DEFAULT 'walk-in' CHECK (type IN ('walk-in', 'online')),
|
||||||
|
triage_category TEXT DEFAULT 'Routine',
|
||||||
|
payment_status TEXT DEFAULT 'unpaid' CHECK (payment_status IN ('unpaid', 'paid')),
|
||||||
|
fee INTEGER DEFAULT 0,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_tenant ON users(tenant_id);
|
CREATE INDEX IF NOT EXISTS idx_users_tenant ON users(tenant_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||||
CREATE INDEX IF NOT EXISTS idx_pending_status ON pending_registrations(status);
|
CREATE INDEX IF NOT EXISTS idx_pending_status ON pending_registrations(status);
|
||||||
|
|
@ -139,6 +165,7 @@ async function seedDemoUsers() {
|
||||||
const hash = await bcrypt.hash(defaultPassword, 10);
|
const hash = await bcrypt.hash(defaultPassword, 10);
|
||||||
const demoUsers = [
|
const demoUsers = [
|
||||||
{ email: 'doctor@curio.app', name: 'Dr. Sharma', role: 'DOCTOR', tenant_id: 'default' },
|
{ email: 'doctor@curio.app', name: 'Dr. Sharma', role: 'DOCTOR', tenant_id: 'default' },
|
||||||
|
{ email: 'reception@curio.app', name: 'Front Desk', role: 'RECEPTIONIST', tenant_id: 'default' },
|
||||||
{ email: 'pharmacy@curio.app', name: 'Pharmacist', role: 'PHARMACIST', tenant_id: 'default' },
|
{ email: 'pharmacy@curio.app', name: 'Pharmacist', role: 'PHARMACIST', tenant_id: 'default' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -499,6 +499,91 @@ app.patch('/api/clinic/agents/:type', requireAuth('OWNER'), async (req, res) =>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── CLINIC OPERATIONS (QUEUE & PATIENTS) ──────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/clinic/queue
|
||||||
|
* Fetch the live queue for the tenant
|
||||||
|
*/
|
||||||
|
app.get('/api/clinic/queue', requireAuth(), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.user.tenantId;
|
||||||
|
const result = await query(
|
||||||
|
`SELECT v.*, p.name as patient_name, p.whatsapp_number, p.age, p.gender, p.display_id
|
||||||
|
FROM visits v
|
||||||
|
JOIN patients p ON v.patient_id = p.id
|
||||||
|
WHERE v.tenant_id = $1 AND v.status IN ('waiting', 'in-room')
|
||||||
|
ORDER BY v.created_at ASC`,
|
||||||
|
[tenantId]
|
||||||
|
);
|
||||||
|
res.json(result.rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Clinic Queue]', err);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch queue' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/clinic/walk-in
|
||||||
|
* Register a new walk-in patient and add to queue
|
||||||
|
*/
|
||||||
|
app.post('/api/clinic/walk-in', requireAuth(), async (req, res) => {
|
||||||
|
const { name, whatsappNumber, age, gender, fee } = req.body;
|
||||||
|
const tenantId = req.user.tenantId;
|
||||||
|
|
||||||
|
if (!name || !whatsappNumber) return res.status(400).json({ error: 'Name and WhatsApp number are required' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await query('BEGIN');
|
||||||
|
|
||||||
|
// Find or create patient
|
||||||
|
let patientResult = await query(
|
||||||
|
'SELECT id FROM patients WHERE tenant_id = $1 AND whatsapp_number = $2',
|
||||||
|
[tenantId, whatsappNumber]
|
||||||
|
);
|
||||||
|
|
||||||
|
let patientId;
|
||||||
|
if (patientResult.rows.length > 0) {
|
||||||
|
patientId = patientResult.rows[0].id;
|
||||||
|
} else {
|
||||||
|
// Generate display ID
|
||||||
|
const countRes = await query('SELECT count(*) FROM patients WHERE tenant_id = $1', [tenantId]);
|
||||||
|
const nextNum = parseInt(countRes.rows[0].count) + 1001;
|
||||||
|
const displayId = `CF-${nextNum}`;
|
||||||
|
|
||||||
|
const insertPatient = await query(
|
||||||
|
`INSERT INTO patients (tenant_id, display_id, name, whatsapp_number, age, gender)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
|
||||||
|
[tenantId, displayId, name, whatsappNumber, age || null, gender || null]
|
||||||
|
);
|
||||||
|
patientId = insertPatient.rows[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Token Number (simple count-based for the day)
|
||||||
|
const todayStr = new Date().toISOString().split('T')[0];
|
||||||
|
const tokenRes = await query(
|
||||||
|
`SELECT count(*) FROM visits WHERE tenant_id = $1 AND DATE(created_at) = $2`,
|
||||||
|
[tenantId, todayStr]
|
||||||
|
);
|
||||||
|
const tokenNum = parseInt(tokenRes.rows[0].count) + 1;
|
||||||
|
const tokenStr = `#${tokenNum.toString().padStart(3, '0')}`;
|
||||||
|
|
||||||
|
// Insert Visit
|
||||||
|
const insertVisit = await query(
|
||||||
|
`INSERT INTO visits (tenant_id, patient_id, token_number, type, status, fee)
|
||||||
|
VALUES ($1, $2, $3, 'walk-in', 'waiting', $4) RETURNING *`,
|
||||||
|
[tenantId, patientId, tokenStr, fee || 0]
|
||||||
|
);
|
||||||
|
|
||||||
|
await query('COMMIT');
|
||||||
|
res.json({ success: true, visit: insertVisit.rows[0], token: tokenStr });
|
||||||
|
} catch (err) {
|
||||||
|
await query('ROLLBACK');
|
||||||
|
console.error('[Clinic Walk-in]', err);
|
||||||
|
res.status(500).json({ error: 'Failed to register walk-in' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ── CLINIC OWNER ROUTES ───────────────────────────────────────────────────────
|
// ── CLINIC OWNER ROUTES ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@
|
||||||
<input type="date" id="queueDate" class="date-input" aria-label="Queue Date">
|
<input type="date" id="queueDate" class="date-input" aria-label="Queue Date">
|
||||||
</div>
|
</div>
|
||||||
<span class="badge counter-badge">Reception Counter: 1</span>
|
<span class="badge counter-badge">Reception Counter: 1</span>
|
||||||
<button class="btn-primary" id="newWalkinBtn">+ New Walk-in (Print Token)</button>
|
<button class="btn-primary" id="newWalkinBtn" onclick="openWalkinModal()">+ New Walk-in (Print Token)</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -125,21 +125,8 @@
|
||||||
<h3>Up Next <small>(Ctrl+Q)</small></h3>
|
<h3>Up Next <small>(Ctrl+Q)</small></h3>
|
||||||
<span class="badge">4 Waiting</span>
|
<span class="badge">4 Waiting</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pipeline-list">
|
<div class="pipeline-list" id="livePipelineList">
|
||||||
<div class="pipeline-item next" onclick="switchPatientContext('Anjali Singh', '#013', 'High Fever')">
|
<!-- Live queue injected here via JS -->
|
||||||
<div class="p-token">#013</div>
|
|
||||||
<div class="p-info">
|
|
||||||
<strong>Anjali Singh</strong>
|
|
||||||
<span class="triage-pill urgent">High Fever</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pipeline-item" onclick="switchPatientContext('Suresh Kumar', '#014', 'Back Pain')">
|
|
||||||
<div class="p-token">#014</div>
|
|
||||||
<div class="p-info">
|
|
||||||
<strong>Suresh Kumar</strong>
|
|
||||||
<span class="triage-pill">Back Pain</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|
@ -306,37 +293,8 @@
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="liveQueueTbody">
|
||||||
<tr class="active-row">
|
<!-- Live queue rows injected here via JS -->
|
||||||
<td>#012</td>
|
|
||||||
<td><strong>Rahul Verma</strong></td>
|
|
||||||
<td><span class="source-tag online">Online</span></td>
|
|
||||||
<td><span class="lang-tag">HI</span></td>
|
|
||||||
<td><span class="triage-pill urgent">High Fever / Cough</span></td>
|
|
||||||
<td><span class="status-pill in-room">In Room</span></td>
|
|
||||||
<td>09:15 AM</td>
|
|
||||||
<td><button class="btn-small">Prescribe</button></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>#013</td>
|
|
||||||
<td><strong>Anjali Singh</strong></td>
|
|
||||||
<td><span class="source-tag walkin">Walk-in</span></td>
|
|
||||||
<td><span class="lang-tag">TE</span></td>
|
|
||||||
<td><span class="triage-pill moderate">General Checkup</span></td>
|
|
||||||
<td><span class="status-pill pending-pay">Unpaid (₹500)</span></td>
|
|
||||||
<td>09:30 AM</td>
|
|
||||||
<td><button class="btn-small secondary">Collect Pay</button></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>#014</td>
|
|
||||||
<td><strong>Suresh Kumar</strong></td>
|
|
||||||
<td><span class="source-tag online">Online</span></td>
|
|
||||||
<td><span class="lang-tag">EN</span></td>
|
|
||||||
<td><span class="triage-pill routine">Back Pain</span></td>
|
|
||||||
<td><span class="status-pill waiting">Paid (Waiting)</span></td>
|
|
||||||
<td>09:45 AM</td>
|
|
||||||
<td><button class="btn-small secondary">Notify</button></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -572,6 +530,51 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Walk-in Modal -->
|
||||||
|
<div id="walkInModal" class="modal">
|
||||||
|
<div class="modal-content" style="max-width: 500px;">
|
||||||
|
<header class="modal-header">
|
||||||
|
<h2>New Walk-in Patient</h2>
|
||||||
|
<button type="button" class="close-modal" onclick="closeWalkinModal()">×</button>
|
||||||
|
</header>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="walkInForm">
|
||||||
|
<div class="input-group">
|
||||||
|
<label>Patient Name *</label>
|
||||||
|
<input type="text" id="walkInName" required placeholder="e.g. Rahul Verma">
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label>WhatsApp Number *</label>
|
||||||
|
<input type="tel" id="walkInPhone" required placeholder="+91">
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 1rem;">
|
||||||
|
<div class="input-group" style="flex:1;">
|
||||||
|
<label>Age</label>
|
||||||
|
<input type="number" id="walkInAge" placeholder="e.g. 32">
|
||||||
|
</div>
|
||||||
|
<div class="input-group" style="flex:1;">
|
||||||
|
<label>Gender</label>
|
||||||
|
<select id="walkInGender">
|
||||||
|
<option value="">Select...</option>
|
||||||
|
<option value="Male">Male</option>
|
||||||
|
<option value="Female">Female</option>
|
||||||
|
<option value="Other">Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label>Consultation Fee Collected (₹)</label>
|
||||||
|
<input type="number" id="walkInFee" value="500">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<footer class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closeWalkinModal()">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="submitWalkIn()" id="submitWalkInBtn">Register & Print Token</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
<script src="dashboard.js?v=100"></script>
|
<script src="dashboard.js?v=100"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
167
dashboard.js
167
dashboard.js
|
|
@ -985,3 +985,170 @@ async function submitPrescription() {
|
||||||
alert('An error occurred while creating the prescription.');
|
alert('An error occurred while creating the prescription.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Live Queue & Walk-in UI Logic
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
let liveQueuePollingInterval = null;
|
||||||
|
|
||||||
|
async function fetchLiveQueue() {
|
||||||
|
const token = localStorage.getItem("curio_token");
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/clinic/queue', {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const queue = await res.json();
|
||||||
|
renderLiveQueueTable(queue);
|
||||||
|
renderZenPipeline(queue);
|
||||||
|
updateQueueStats(queue);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch live queue:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateQueueStats(queue) {
|
||||||
|
const counterBadge = document.querySelector('.counter-badge');
|
||||||
|
if (counterBadge) counterBadge.innerText = `Waiting: ${queue.filter(q => q.status === 'waiting').length}`;
|
||||||
|
|
||||||
|
const waitingSpan = document.querySelector('.pipeline-header .badge');
|
||||||
|
if (waitingSpan) waitingSpan.innerText = `${queue.filter(q => q.status === 'waiting').length} Waiting`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLiveQueueTable(queue) {
|
||||||
|
const tbody = document.getElementById("liveQueueTbody");
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
if (queue.length === 0) {
|
||||||
|
tbody.innerHTML = `<tr><td colspan="8" style="text-align: center; color: var(--text-muted);">Queue is empty</td></tr>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = queue.map(visit => {
|
||||||
|
const time = new Date(visit.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||||
|
const sourceClass = visit.type === 'online' ? 'online' : 'walkin';
|
||||||
|
const sourceText = visit.type === 'online' ? 'Online' : 'Walk-in';
|
||||||
|
|
||||||
|
let statusHtml = '';
|
||||||
|
if (visit.status === 'in-room') {
|
||||||
|
statusHtml = `<span class="status-pill in-room">In Room</span>`;
|
||||||
|
} else if (visit.payment_status === 'unpaid') {
|
||||||
|
statusHtml = `<span class="status-pill pending-pay">Unpaid (₹${visit.fee})</span>`;
|
||||||
|
} else {
|
||||||
|
statusHtml = `<span class="status-pill waiting">Paid (Waiting)</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionHtml = '';
|
||||||
|
if (visit.status === 'in-room') {
|
||||||
|
actionHtml = `<button class="btn-small" onclick="showSection('zen'); switchPatientContext('${visit.patient_name}', '${visit.token_number}', '${visit.triage_category}')">Consult</button>`;
|
||||||
|
} else if (visit.payment_status === 'unpaid') {
|
||||||
|
actionHtml = `<button class="btn-small secondary" onclick="alert('Collect Pay Flow...')">Collect Pay</button>`;
|
||||||
|
} else {
|
||||||
|
actionHtml = `<button class="btn-small secondary" onclick="alert('Notifying patient...')">Notify</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr class="${visit.status === 'in-room' ? 'active-row' : ''}">
|
||||||
|
<td>${visit.token_number}</td>
|
||||||
|
<td><strong>${visit.patient_name}</strong><br><small class="text-muted">${visit.display_id}</small></td>
|
||||||
|
<td><span class="source-tag ${sourceClass}">${sourceText}</span></td>
|
||||||
|
<td><span class="lang-tag">EN</span></td>
|
||||||
|
<td><span class="triage-pill ${visit.triage_category === 'Urgent' ? 'urgent' : 'routine'}">${visit.triage_category || 'Routine'}</span></td>
|
||||||
|
<td>${statusHtml}</td>
|
||||||
|
<td>${time}</td>
|
||||||
|
<td>${actionHtml}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderZenPipeline(queue) {
|
||||||
|
const pipelineList = document.getElementById("livePipelineList");
|
||||||
|
if (!pipelineList) return;
|
||||||
|
|
||||||
|
if (queue.length === 0) {
|
||||||
|
pipelineList.innerHTML = `<div style="padding: 1rem; color: var(--text-muted); text-align: center; font-size: 0.9rem;">No patients waiting</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pipelineList.innerHTML = queue.map((visit, index) => {
|
||||||
|
const isNext = index === 0 && visit.status !== 'in-room';
|
||||||
|
const isActive = visit.status === 'in-room';
|
||||||
|
const itemClass = isActive ? 'active' : (isNext ? 'next' : '');
|
||||||
|
const triageClass = visit.triage_category === 'Urgent' ? 'urgent' : 'routine';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="pipeline-item ${itemClass}" onclick="switchPatientContext('${visit.patient_name}', '${visit.token_number}', '${visit.triage_category}')">
|
||||||
|
<div class="p-token">${visit.token_number}</div>
|
||||||
|
<div class="p-info">
|
||||||
|
<strong>${visit.patient_name}</strong>
|
||||||
|
<span class="triage-pill ${triageClass}">${visit.triage_category || 'Routine'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk-in Modal Handlers
|
||||||
|
window.openWalkinModal = () => {
|
||||||
|
document.getElementById('walkInModal')?.classList.add('open');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeWalkinModal = () => {
|
||||||
|
document.getElementById('walkInModal')?.classList.remove('open');
|
||||||
|
document.getElementById('walkInForm')?.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.submitWalkIn = async () => {
|
||||||
|
const btn = document.getElementById("submitWalkInBtn");
|
||||||
|
const name = document.getElementById("walkInName").value.trim();
|
||||||
|
const phone = document.getElementById("walkInPhone").value.trim();
|
||||||
|
const age = document.getElementById("walkInAge").value;
|
||||||
|
const gender = document.getElementById("walkInGender").value;
|
||||||
|
const fee = document.getElementById("walkInFee").value;
|
||||||
|
|
||||||
|
if (!name || !phone) {
|
||||||
|
alert("Name and WhatsApp Number are required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerText = "Registering...";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("curio_token");
|
||||||
|
const res = await fetch('/api/clinic/walk-in', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name, whatsappNumber: phone, age, gender, fee: parseInt(fee) || 0 })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
closeWalkinModal();
|
||||||
|
fetchLiveQueue(); // Instantly refresh
|
||||||
|
alert(`Token ${data.token} generated for ${name}!`);
|
||||||
|
} else {
|
||||||
|
alert("Error: " + data.error);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Walk-in error:", err);
|
||||||
|
alert("Failed to register walk-in.");
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerText = "Register & Print Token";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start Polling when DOM loads
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
fetchLiveQueue();
|
||||||
|
liveQueuePollingInterval = setInterval(fetchLiveQueue, 10000); // Poll every 10s
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue