let html5QrCode = null; let currentToken = null; let availableCameras = []; let autoResetTimer = null; const csrfToken = document.querySelector('meta[name="csrf-token"]').content; const qrReader = document.getElementById("qr-reader"); const cameraSelect = document.getElementById("camera-select"); const startScanBtn = document.getElementById("start-scan"); const stopScanBtn = document.getElementById("stop-scan"); const searchInput = document.getElementById("search-input"); const searchResults = document.getElementById("search-results"); const guestCard = document.getElementById("guest-card"); const guestPhoto = document.getElementById("guest-photo"); const guestName = document.getElementById("guest-name"); const guestDepartment = document.getElementById("guest-department"); const guestPosition = document.getElementById("guest-position"); const guestEmail = document.getElementById("guest-email"); const guestEvent = document.getElementById("guest-event"); const eventSelect = document.getElementById("event-select"); const checkInStatus = document.getElementById("check-in-status"); const checkInBtn = document.getElementById("check-in-btn"); const undoCheckInBtn = document.getElementById("undo-check-in-btn"); const resetBtn = document.getElementById("reset-btn"); function signalResult(success) { if (navigator.vibrate) navigator.vibrate(success ? 180 : [100, 80, 100]); try { const AudioContext = window.AudioContext || window.webkitAudioContext; const context = new AudioContext(); const oscillator = context.createOscillator(); const gain = context.createGain(); oscillator.frequency.value = success ? 880 : 220; gain.gain.setValueAtTime(0.12, context.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, context.currentTime + 0.2); oscillator.connect(gain); gain.connect(context.destination); oscillator.onended = () => context.close(); oscillator.start(); oscillator.stop(context.currentTime + 0.2); } catch (_error) {} } function scheduleReset(delay = 8000) { clearTimeout(autoResetTimer); autoResetTimer = setTimeout(resetGuestCard, delay); } function resetGuestCard() { clearTimeout(autoResetTimer); guestCard.classList.add("d-none"); currentToken = null; guestPhoto.src = ""; guestName.textContent = ""; guestDepartment.textContent = ""; guestPosition.textContent = ""; guestEmail.textContent = ""; const placeholder = guestPhoto.nextElementSibling; if (placeholder && placeholder.classList.contains("guest-photo-placeholder")) placeholder.remove(); if (guestEvent) guestEvent.textContent = ""; checkInStatus.classList.add("d-none"); checkInStatus.className = "alert d-none"; checkInBtn.disabled = false; checkInBtn.textContent = "Отметить вход"; undoCheckInBtn.classList.add("d-none"); } function showGuest(guest) { clearTimeout(autoResetTimer); currentToken = guest.token; guestCard.classList.remove("d-none"); if (guest.photo_url) { guestPhoto.src = guest.photo_url; guestPhoto.classList.remove("d-none"); const placeholder = guestPhoto.nextElementSibling; if (placeholder && placeholder.classList.contains("guest-photo-placeholder")) placeholder.remove(); } else { guestPhoto.src = ""; guestPhoto.classList.add("d-none"); // Show placeholder if (!guestPhoto.nextElementSibling || !guestPhoto.nextElementSibling.classList.contains("guest-photo-placeholder")) { const placeholder = document.createElement("div"); placeholder.className = "guest-photo-placeholder mx-auto mb-3"; placeholder.textContent = "Нет фото"; guestPhoto.parentNode.insertBefore(placeholder, guestPhoto.nextSibling); } } guestName.textContent = guest.full_name; guestDepartment.textContent = guest.department; guestPosition.textContent = guest.position || ""; guestEmail.textContent = guest.email; if (guestEvent) { guestEvent.textContent = guest.event_name ? `Мероприятие: ${guest.event_name}` : ""; } if (guest.checked_in) { checkInStatus.classList.remove("d-none", "alert-success", "alert-warning"); checkInStatus.classList.add("alert-warning"); checkInStatus.textContent = "Гость уже отмечен как вошедший"; checkInBtn.disabled = true; undoCheckInBtn.classList.remove("d-none"); } else { checkInStatus.classList.add("d-none"); checkInBtn.disabled = false; undoCheckInBtn.classList.add("d-none"); } window.scrollTo({ top: guestCard.offsetTop - 20, behavior: "smooth" }); } function loadGuestByToken(token) { fetch(`/api/guest/${token}`) .then(response => { if (!response.ok) throw new Error("Гость не найден"); return response.json(); }) .then(guest => showGuest(guest)) .catch(error => { alert(error.message); }); } // Camera selection function populateCameras() { Html5Qrcode.getCameras() .then(cameras => { availableCameras = cameras || []; cameraSelect.innerHTML = ''; if (availableCameras.length === 0) { cameraSelect.innerHTML += ''; return; } availableCameras.forEach((camera, index) => { const option = document.createElement("option"); option.value = camera.id; option.textContent = camera.label || `Камера ${index + 1}`; cameraSelect.appendChild(option); }); // Try to select back camera by default const backCamera = availableCameras.find(c => c.label.toLowerCase().includes("back") || c.label.toLowerCase().includes("задняя") || c.label.toLowerCase().includes("environment") ); if (backCamera) { cameraSelect.value = backCamera.id; } else if (availableCameras.length > 0) { cameraSelect.value = availableCameras[availableCameras.length - 1].id; } }) .catch(err => { console.error("Ошибка получения списка камер:", err); cameraSelect.innerHTML = ''; }); } // QR Scanner startScanBtn.addEventListener("click", () => { const cameraId = cameraSelect.value; if (!cameraId) { alert("Сначала выберите камеру"); return; } html5QrCode = new Html5Qrcode("qr-reader"); html5QrCode.start( cameraId, { fps: 10, qrbox: { width: 250, height: 250 } }, (decodedText) => { html5QrCode.stop(); stopScanBtn.classList.add("d-none"); startScanBtn.classList.remove("d-none"); // Extract token from URL try { const url = new URL(decodedText); const token = url.searchParams.get("token"); if (token) { loadGuestByToken(token); } } catch (e) { // If decodedText is not a URL, treat it as token loadGuestByToken(decodedText); } }, (errorMessage) => {} ); startScanBtn.classList.add("d-none"); stopScanBtn.classList.remove("d-none"); }); stopScanBtn.addEventListener("click", () => { if (html5QrCode) { html5QrCode.stop(); } stopScanBtn.classList.add("d-none"); startScanBtn.classList.remove("d-none"); }); // Search by name let searchTimeout = null; searchInput.addEventListener("input", (e) => { const query = e.target.value.trim(); searchResults.innerHTML = ""; if (query.length < 2) return; clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { const eventId = eventSelect ? eventSelect.value : ""; let url = `/api/guests/search?q=${encodeURIComponent(query)}`; if (eventId) { url += `&event_id=${encodeURIComponent(eventId)}`; } fetch(url) .then(response => { if (!response.ok) { return response.json().then(data => { throw new Error(data.error || "Ошибка поиска"); }); } return response.json(); }) .then(guests => { searchResults.innerHTML = ""; guests.forEach(guest => { const item = document.createElement("button"); item.className = "list-group-item list-group-item-action"; const name = document.createElement("strong"); name.textContent = guest.full_name; const department = document.createElement("small"); department.textContent = guest.department || ""; item.append(name, document.createElement("br"), department); item.addEventListener("click", () => { searchResults.innerHTML = ""; searchInput.value = ""; showGuest(guest); }); searchResults.appendChild(item); }); }) .catch(error => { const item = document.createElement("div"); item.className = "list-group-item text-danger"; item.textContent = error.message; searchResults.replaceChildren(item); }); }, 300); }); // Check-in button checkInBtn.addEventListener("click", () => { if (!currentToken) return; checkInBtn.disabled = true; checkInBtn.textContent = "Отметка..."; fetch(`/api/check-in/${currentToken}`, { method: "POST", headers: { "X-CSRFToken": csrfToken }, }) .then(response => response.json().then(data => ({ status: response.status, data }))) .then(({ status, data }) => { checkInStatus.classList.remove("d-none", "alert-success", "alert-warning", "alert-danger"); if (data.success) { checkInStatus.classList.add("alert-success"); checkInStatus.textContent = "Вход успешно отмечен!"; checkInBtn.textContent = "Вход отмечен"; undoCheckInBtn.classList.remove("d-none"); signalResult(true); scheduleReset(); } else if (status === 409) { checkInStatus.classList.add("alert-warning"); checkInStatus.textContent = "Гость уже отмечен как вошедший"; checkInBtn.textContent = "Уже отмечен"; undoCheckInBtn.classList.remove("d-none"); signalResult(false); scheduleReset(); } else { checkInStatus.classList.add("alert-danger"); checkInStatus.textContent = data.message || "Ошибка"; checkInBtn.disabled = false; checkInBtn.textContent = "Отметить вход"; } }) .catch(error => { checkInStatus.classList.remove("d-none", "alert-success", "alert-warning", "alert-danger"); checkInStatus.classList.add("alert-danger"); checkInStatus.textContent = "Ошибка сети: " + error.message; checkInBtn.disabled = false; checkInBtn.textContent = "Отметить вход"; }); }); undoCheckInBtn.addEventListener("click", () => { if (!currentToken) return; clearTimeout(autoResetTimer); const reason = window.prompt("Укажите причину отмены отметки входа:"); if (!reason || reason.trim().length < 3) return; undoCheckInBtn.disabled = true; fetch(`/api/check-in/${currentToken}/undo`, { method: "POST", headers: {"Content-Type": "application/json", "X-CSRFToken": csrfToken}, body: JSON.stringify({reason: reason.trim()}), }) .then(response => response.json().then(data => ({status: response.status, data}))) .then(({status, data}) => { checkInStatus.classList.remove("d-none", "alert-success", "alert-warning", "alert-danger"); if (status === 200 && data.success) { checkInStatus.classList.add("alert-warning"); checkInStatus.textContent = "Отметка входа отменена"; checkInBtn.disabled = false; checkInBtn.textContent = "Отметить вход"; undoCheckInBtn.classList.add("d-none"); } else { checkInStatus.classList.add("alert-danger"); checkInStatus.textContent = data.message || "Не удалось отменить отметку"; } }) .catch(error => { checkInStatus.className = "alert alert-danger"; checkInStatus.textContent = "Ошибка сети: " + error.message; }) .finally(() => { undoCheckInBtn.disabled = false; }); }); resetBtn.addEventListener("click", resetGuestCard); // If token is in URL (scanned via external scanner) document.addEventListener("DOMContentLoaded", () => { populateCameras(); const urlParams = new URLSearchParams(window.location.search); const token = urlParams.get("token"); if (token) { loadGuestByToken(token); } });