// main.js // JavaScript for Attendance System // IndexedDB helper class AttendanceDB { constructor() { this.dbName = 'attendanceDB'; this.storeName = 'attendance'; this.db = null; } async open() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, 1); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(this.storeName)) { db.createObjectStore(this.storeName, {keyPath: 'id', autoIncrement: true}); } }; request.onsuccess = (event) => { this.db = event.target.result; resolve(); }; request.onerror = (event) => { reject(event.target.error); }; }); } async addRecord(data) { return new Promise((resolve, reject) => { const tx = this.db.transaction([this.storeName], 'readwrite'); const store = tx.objectStore(this.storeName); const req = store.add(data); req.onsuccess = () => resolve(); req.onerror = (e) => reject(e.target.error); }); } async getAllRecords() { return new Promise((resolve, reject) => { const tx = this.db.transaction([this.storeName], 'readonly'); const store = tx.objectStore(this.storeName); const req = store.getAll(); req.onsuccess = (e) => resolve(e.target.result); req.onerror = (e) => reject(e.target.error); }); } } class AttendanceSystem { constructor() { this.video = document.getElementById('video'); this.faceOverlay = document.getElementById('faceOverlay'); this.startCameraBtn = document.getElementById('startCamera'); this.checkInBtn = document.getElementById('checkInBtn'); this.checkOutBtn = document.getElementById('checkOutBtn'); this.loadingIndicator = document.getElementById('loadingIndicator'); this.alertContainer = document.getElementById('alertContainer'); this.currentLocation = null; this.isFaceDetected = false; this.isCheckedIn = false; this.faceDetectionInterval = null; this.faceApiLoaded = false; this.snapshotCanvas = document.getElementById('snapshotCanvas'); this.db = new AttendanceDB(); this.db.open().then(() => { this.init(); }); } init() { this.loadFaceApiModels(); this.updateDateTime(); this.updateNetworkStatus(); this.loadAttendanceHistory(); this.updateDailySummary(); this.getCurrentLocation(); this.checkTodayStatus(); // Event listeners this.startCameraBtn.addEventListener('click', () => this.startCamera()); this.checkInBtn.addEventListener('click', () => this.checkIn()); this.checkOutBtn.addEventListener('click', () => this.checkOut()); // Network status listeners window.addEventListener('online', () => { this.updateNetworkStatus(); if (!this.faceApiLoaded) { this.showAlert('กลับมาออนไลน์แล้ว กำลังโหลด Face Detection ใหม่...', 'info'); this.loadFaceApiModels(); } }); window.addEventListener('offline', () => { this.updateNetworkStatus(); this.showAlert('หลุดการเชื่อมต่อ internet จะใช้โหมดจำลอง', 'warning'); }); // Update time every second setInterval(() => this.updateDateTime(), 1000); // Check geolocation every 5 minutes setInterval(() => this.getCurrentLocation(), 300000); } async loadFaceApiModels() { try { this.showLoading(true); // Check network connection first if (!this.checkNetworkConnection()) { throw new Error('ไม่มีการเชื่อมต่อ internet'); } this.updateFaceStatus('กำลังโหลด Face Detection จาก CDN...'); // Try multiple CDN sources for better reliability const modelSources = [ 'https://raw.githubusercontent.com/justadudewhohacks/face-api.js/master/weights', 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@master/weights' ]; let modelsLoaded = false; for (const source of modelSources) { try { this.updateFaceStatus(`กำลังโหลด SSD MobileNet จาก ${source.includes('github') ? 'GitHub' : 'JSDelivr'}...`); await faceapi.nets.ssdMobilenetv1.loadFromUri(source); this.updateFaceStatus(`กำลังโหลด Face Landmarks จาก ${source.includes('github') ? 'GitHub' : 'JSDelivr'}...`); await faceapi.nets.faceLandmark68Net.loadFromUri(source); modelsLoaded = true; break; } catch (sourceError) { console.warn(`Failed to load from ${source}:`, sourceError); if (source === modelSources[modelSources.length - 1]) { throw sourceError; // Throw the last error if all sources fail } } } if (!modelsLoaded) { throw new Error('ไม่สามารถโหลด models จาก CDN ใดๆ ได้'); } this.faceApiLoaded = true; this.updateFaceStatus('Face Detection พร้อมใช้งาน'); this.showAlert('โหลด Face Detection สำเร็จ', 'success'); } catch (error) { console.error('Face API loading error:', error); this.faceApiLoaded = false; if (error.message.includes('internet')) { this.updateFaceStatus('ไม่มี internet - ใช้โหมดจำลอง'); this.showAlert('ไม่มีการเชื่อมต่อ internet จะใช้โหมดจำลอง', 'warning'); } else { this.updateFaceStatus('Face Detection ล้มเหลว - ใช้โหมดจำลอง'); this.showAlert('ไม่สามารถโหลด Face Detection ได้ จะใช้โหมดจำลอง', 'warning'); } } finally { this.showLoading(false); } } updateDateTime() { const now = new Date(); const dateOptions = { year: 'numeric', month: 'long', day: 'numeric', locale: 'th-TH' }; const timeOptions = { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; document.getElementById('currentDate').textContent = now.toLocaleDateString('th-TH', dateOptions); document.getElementById('currentTime').textContent = now.toLocaleTimeString('th-TH', timeOptions); } async startCamera() { try { this.showLoading(true); const stream = await navigator.mediaDevices.getUserMedia({ video: { width: {ideal: 1280}, height: {ideal: 720}, facingMode: 'user' } }); this.video.srcObject = stream; this.startCameraBtn.disabled = true; this.startCameraBtn.textContent = 'กล้องเปิดแล้ว'; // Wait for video to load before starting face detection this.video.addEventListener('loadeddata', () => { this.startFaceDetection(); }); this.showAlert('เปิดกล้องสำเร็จ', 'success'); } catch (error) { console.error('Camera error:', error); this.showAlert('ไม่สามารถเปิดกล้องได้ กรุณาตรวจสอบการอนุญาต', 'error'); } finally { this.showLoading(false); } } startFaceDetection() { if (!this.faceApiLoaded) { // Fallback to simulation if face-api.js is not loaded this.startSimulatedFaceDetection(); return; } // Real face detection using face-api.js this.faceDetectionInterval = setInterval(async () => { try { const detections = await faceapi .detectAllFaces(this.video, new faceapi.SsdMobilenetv1Options({minConfidence: 0.5})) .withFaceLandmarks(); // Clear previous face boxes this.faceOverlay.innerHTML = ''; if (detections.length > 0) { const detection = detections[0]; // Use the first detection const confidence = Math.round(detection.detection.score * 100); if (!this.isFaceDetected) { this.isFaceDetected = true; this.updateFaceStatus(`ตรวจพบใบหน้า (${confidence}%)`); this.checkInBtn.disabled = false; this.checkOutBtn.disabled = false; } else { this.updateFaceStatus(`ตรวจพบใบหน้า (${confidence}%)`); } // Draw face detection boxes detections.forEach(detection => { this.drawFaceBox(detection.detection.box, detection.detection.score); }); } else { if (this.isFaceDetected) { this.isFaceDetected = false; this.updateFaceStatus('ไม่พบใบหน้า'); this.checkInBtn.disabled = true; this.checkOutBtn.disabled = true; } } } catch (error) { console.error('Face detection error:', error); // Fallback to simulation on error this.startSimulatedFaceDetection(); } }, 500); // Check every 500ms for better performance } startSimulatedFaceDetection() { // Fallback simulation mode this.faceDetectionInterval = setInterval(() => { const isDetected = Math.random() > 0.3; // 70% chance of detection if (isDetected && !this.isFaceDetected) { this.isFaceDetected = true; this.showSimulatedFaceBox(); this.updateFaceStatus('ตรวจพบใบหน้า (โหมดจำลอง)'); this.checkInBtn.disabled = false; this.checkOutBtn.disabled = false; } else if (!isDetected && this.isFaceDetected) { this.isFaceDetected = false; this.hideFaceBox(); this.updateFaceStatus('ไม่พบใบหน้า (โหมดจำลอง)'); this.checkInBtn.disabled = true; this.checkOutBtn.disabled = true; } }, 1000); } drawFaceBox(box, confidence = 1) { const faceBox = document.createElement('div'); faceBox.className = 'face-box'; // Calculate position relative to video element const videoRect = this.video.getBoundingClientRect(); const videoDisplayWidth = this.video.offsetWidth; const videoDisplayHeight = this.video.offsetHeight; // Scale detection box to video display size const scaleX = videoDisplayWidth / this.video.videoWidth; const scaleY = videoDisplayHeight / this.video.videoHeight; faceBox.style.left = (box.x * scaleX) + 'px'; faceBox.style.top = (box.y * scaleY) + 'px'; faceBox.style.width = (box.width * scaleX) + 'px'; faceBox.style.height = (box.height * scaleY) + 'px'; // Add confidence indicator if (confidence < 1) { const confidenceLabel = document.createElement('div'); confidenceLabel.className = 'confidence-label'; confidenceLabel.textContent = `${Math.round(confidence * 100)}%`; confidenceLabel.style.position = 'absolute'; confidenceLabel.style.top = '-25px'; confidenceLabel.style.left = '0'; confidenceLabel.style.background = 'rgba(16, 185, 129, 0.9)'; confidenceLabel.style.color = 'white'; confidenceLabel.style.padding = '2px 6px'; confidenceLabel.style.borderRadius = '4px'; confidenceLabel.style.fontSize = '12px'; confidenceLabel.style.fontWeight = 'bold'; faceBox.appendChild(confidenceLabel); } this.faceOverlay.appendChild(faceBox); } showSimulatedFaceBox() { // Create simulated face detection box for fallback mode const faceBox = document.createElement('div'); faceBox.className = 'face-box'; faceBox.style.left = '25%'; faceBox.style.top = '20%'; faceBox.style.width = '50%'; faceBox.style.height = '60%'; this.faceOverlay.innerHTML = ''; this.faceOverlay.appendChild(faceBox); } hideFaceBox() { this.faceOverlay.innerHTML = ''; } updateFaceStatus(status) { document.getElementById('faceStatus').textContent = status; } async getCurrentLocation() { try { const position = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition(resolve, reject, { enableHighAccuracy: true, timeout: 10000, maximumAge: 300000 }); }); this.currentLocation = { latitude: position.coords.latitude, longitude: position.coords.longitude, accuracy: position.coords.accuracy, timestamp: new Date().toISOString() }; document.getElementById('locationInfo').textContent = `ละติจูด: ${position.coords.latitude.toFixed(6)}, ลองจิจูด: ${position.coords.longitude.toFixed(6)}`; document.getElementById('locationAccuracy').textContent = `ความแม่นยำ: ${Math.round(position.coords.accuracy)} เมตร`; } catch (error) { console.error('Location error:', error); document.getElementById('locationInfo').textContent = 'ไม่สามารถหาตำแหน่งได้'; document.getElementById('locationAccuracy').textContent = 'กรุณาเปิดการใช้งานตำแหน่ง'; } } async checkIn() { if (!this.isFaceDetected) { this.showAlert('กรุณาตรวจสอบใบหน้าก่อน', 'warning'); return; } if (!this.currentLocation) { this.showAlert('กรุณารอการตรวจสอบตำแหน่ง', 'warning'); return; } this.showLoading(true); this.showAlert('กำลังบันทึกการเข้างาน...', 'info'); const faceImage = await this.captureFaceImage(); setTimeout(async () => { const attendanceData = { type: 'check-in', timestamp: new Date().toISOString(), location: this.currentLocation, faceDetected: this.isFaceDetected, faceImage: faceImage, detectionMethod: this.faceApiLoaded ? 'face-api.js' : 'simulation' }; await this.db.addRecord(attendanceData); this.isCheckedIn = true; this.updateCurrentStatus('เข้างานแล้ว'); // Enhanced success feedback this.showSuccessNotification('✅ ลงเวลาเข้างานสำเร็จ!', `เวลา: ${new Date().toLocaleTimeString('th-TH')}`); this.checkInBtn.style.background = '#10b981'; this.checkInBtn.textContent = '✓ เข้างานสำเร็จ'; // Reset button after 3 seconds setTimeout(() => { this.checkInBtn.style.background = ''; this.checkInBtn.textContent = 'เข้างาน'; }, 3000); this.loadAttendanceHistory(); this.updateDailySummary(); this.showLoading(false); }, 2000); } async checkOut() { if (!this.isFaceDetected) { this.showAlert('กรุณาตรวจสอบใบหน้าก่อน', 'warning'); return; } if (!this.currentLocation) { this.showAlert('กรุณารอการตรวจสอบตำแหน่ง', 'warning'); return; } this.showLoading(true); this.showAlert('กำลังบันทึกการออกงาน...', 'info'); const faceImage = await this.captureFaceImage(); setTimeout(async () => { const attendanceData = { type: 'check-out', timestamp: new Date().toISOString(), location: this.currentLocation, faceDetected: this.isFaceDetected, faceImage: faceImage, detectionMethod: this.faceApiLoaded ? 'face-api.js' : 'simulation' }; await this.db.addRecord(attendanceData); this.isCheckedIn = false; this.updateCurrentStatus('ออกงานแล้ว'); // Enhanced success feedback this.showSuccessNotification('✅ ลงเวลาออกงานสำเร็จ!', `เวลา: ${new Date().toLocaleTimeString('th-TH')}`); this.checkOutBtn.style.background = '#ef4444'; this.checkOutBtn.textContent = '✓ ออกงานสำเร็จ'; // Reset button after 3 seconds setTimeout(() => { this.checkOutBtn.style.background = ''; this.checkOutBtn.textContent = 'ออกงาน'; }, 3000); this.loadAttendanceHistory(); this.updateDailySummary(); this.showLoading(false); }, 2000); } async captureFaceImage() { // Capture current frame from video as dataURL const video = this.video; const canvas = this.snapshotCanvas; canvas.width = video.videoWidth || 480; canvas.height = video.videoHeight || 360; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); // Optionally crop to face area if needed return canvas.toDataURL('image/jpeg', 0.85); } // เพิ่มฟังก์ชันรองรับการกรองประวัติด้วย type (all/check-in/check-out) async loadAttendanceHistory(type = 'all') { const historyContainer = document.getElementById('attendanceHistory'); let history = []; try { history = await this.db.getAllRecords(); } catch (e) { history = []; } if (!history || history.length === 0) { historyContainer.innerHTML = '
ยังไม่มีประวัติการลงเวลา
วันที่: ${formattedDate}
เวลา: ${formattedTime}
ตำแหน่ง: ${record.location.latitude.toFixed(6)}, ${record.location.longitude.toFixed(6)}
ความแม่นยำ: ${Math.round(record.location.accuracy)} เมตร
การตรวจจับ: ${record.detectionMethod || 'simulation'}
${subtitle}