// 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 = '<div class="empty-state"><p>ยังไม่มีประวัติการลงเวลา</p></div>';
return;
}
historyContainer.innerHTML = '';
history.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
let filtered = history;
if (type === 'check-in') filtered = history.filter(r => r.type === 'check-in');
if (type === 'check-out') filtered = history.filter(r => r.type === 'check-out');
filtered.slice(0, 50).forEach(record => {
const historyItem = document.createElement('div');
historyItem.className = `history-item ${record.type}`;
const date = new Date(record.timestamp);
const formattedDate = date.toLocaleDateString('th-TH', {
year: 'numeric', month: 'long', day: 'numeric'
});
const formattedTime = date.toLocaleTimeString('th-TH', {
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
historyItem.innerHTML = `
<div class="history-details">
<h4>${record.type === 'check-in' ? 'เข้างาน' : 'ออกงาน'}</h4>
<p><strong>วันที่:</strong> ${formattedDate}</p>
<p><strong>เวลา:</strong> ${formattedTime}</p>
<p><strong>ตำแหน่ง:</strong> ${record.location.latitude.toFixed(6)}, ${record.location.longitude.toFixed(6)}</p>
<p><strong>ความแม่นยำ:</strong> ${Math.round(record.location.accuracy)} เมตร</p>
<p><strong>การตรวจจับ:</strong> ${record.detectionMethod || 'simulation'}</p>
</div>
<div class="history-face">
${record.faceImage ? `<img src="${record.faceImage}" alt="face" />` : ''}
</div>
`;
historyContainer.appendChild(historyItem);
});
}
async checkTodayStatus() {
let history = [];
try {
history = await this.db.getAllRecords();
} catch (e) {
history = [];
}
const today = new Date().toDateString();
const todayRecords = history.filter(record => {
const recordDate = new Date(record.timestamp).toDateString();
return recordDate === today;
});
const lastCheckIn = todayRecords.find(record => record.type === 'check-in');
const lastCheckOut = todayRecords.find(record => record.type === 'check-out');
if (lastCheckIn && !lastCheckOut) {
this.isCheckedIn = true;
this.updateCurrentStatus('เข้างานแล้ว');
} else if (lastCheckOut) {
this.isCheckedIn = false;
this.updateCurrentStatus('ออกงานแล้ว');
}
}
updateCurrentStatus(status) {
document.getElementById('currentStatus').textContent = status;
}
showAlert(message, type) {
const alert = document.createElement('div');
alert.className = `alert ${type}`;
alert.textContent = message;
this.alertContainer.innerHTML = '';
this.alertContainer.appendChild(alert);
setTimeout(() => {
alert.remove();
}, 5000);
}
showSuccessNotification(title, subtitle) {
// Create enhanced success notification
const notification = document.createElement('div');
notification.className = 'success-notification';
notification.innerHTML = `
<div class="notification-icon">🎉</div>
<div class="notification-content">
<h3>${title}</h3>
<p>${subtitle}</p>
</div>
`;
// Add to container
this.alertContainer.innerHTML = '';
this.alertContainer.appendChild(notification);
// Add entrance animation
notification.style.transform = 'translateY(-20px)';
notification.style.opacity = '0';
setTimeout(() => {
notification.style.transform = 'translateY(0)';
notification.style.opacity = '1';
}, 100);
// Auto remove after 5 seconds
setTimeout(() => {
notification.style.transform = 'translateY(-20px)';
notification.style.opacity = '0';
setTimeout(() => notification.remove(), 300);
}, 5000);
// Add vibration if supported
if (navigator.vibrate) {
navigator.vibrate([200, 100, 200]);
}
// Play success sound
this.playSuccessSound();
}
playSuccessSound() {
// Create a simple success tone using Web Audio API
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
// Create success melody
const frequencies = [523.25, 659.25, 783.99]; // C5, E5, G5
const duration = 0.15;
frequencies.forEach((freq, index) => {
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.setValueAtTime(freq, audioContext.currentTime + index * duration);
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0, audioContext.currentTime + index * duration);
gainNode.gain.linearRampToValueAtTime(0.1, audioContext.currentTime + index * duration + 0.01);
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + index * duration + duration);
oscillator.start(audioContext.currentTime + index * duration);
oscillator.stop(audioContext.currentTime + index * duration + duration);
});
} catch (error) {
console.log('Audio not supported:', error);
}
}
showLoading(show) {
this.loadingIndicator.style.display = show ? 'block' : 'none';
}
// เพิ่มฟังก์ชันตรวจสอบการเชื่อมต่อ network
checkNetworkConnection() {
return navigator.onLine;
}
updateNetworkStatus() {
const isOnline = this.checkNetworkConnection();
const statusElement = document.getElementById('networkStatus');
if (statusElement) {
statusElement.textContent = isOnline ? 'ออนไลน์' : 'ออฟไลน์';
statusElement.style.color = isOnline ? '#10b981' : '#ef4444';
}
}
updateDailySummary() {
this.db.getAllRecords().then(history => {
const today = new Date().toDateString();
const todayRecords = history.filter(record => {
const recordDate = new Date(record.timestamp).toDateString();
return recordDate === today;
});
const checkInRecord = todayRecords.find(r => r.type === 'check-in');
const checkOutRecord = todayRecords.find(r => r.type === 'check-out');
document.getElementById('todayCheckIn').textContent =
checkInRecord ? new Date(checkInRecord.timestamp).toLocaleTimeString('th-TH', {hour: '2-digit', minute: '2-digit'}) : '-';
document.getElementById('todayCheckOut').textContent =
checkOutRecord ? new Date(checkOutRecord.timestamp).toLocaleTimeString('th-TH', {hour: '2-digit', minute: '2-digit'}) : '-';
}).catch(() => {
document.getElementById('todayCheckIn').textContent = '-';
document.getElementById('todayCheckOut').textContent = '-';
});
}
}
// ให้ AttendanceSystem เป็น global เพื่อให้สคริปต์ใน index.html เรียกใช้ได้
window.attendanceSystem = new AttendanceSystem();
// Initialize the system when page loads
window.addEventListener('DOMContentLoaded', () => {
new AttendanceSystem();
});