Subh775's picture
Update templates/index.html
742e24c verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Smart Attendance System</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
backdrop-filter: blur(10px);
}
.header {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
font-weight: 600;
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
padding: 30px;
background: #f8fafc;
}
.stat-card {
background: white;
padding: 25px;
border-radius: 15px;
text-align: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.stat-number {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 10px;
}
.stat-label {
color: #64748b;
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.total { color: #3b82f6; }
.present { color: #10b981; }
.absent { color: #ef4444; }
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
padding: 30px;
}
.section {
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.section h2 {
color: #1e293b;
margin-bottom: 20px;
font-size: 1.5rem;
font-weight: 600;
}
.camera-container {
position: relative;
margin-bottom: 20px;
}
#video {
width: 100%;
max-width: 400px;
border-radius: 10px;
border: 3px solid #e2e8f0;
}
.camera-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 200px;
height: 200px;
border: 2px dashed #4f46e5;
border-radius: 50%;
pointer-events: none;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
color: white;
}
.btn-success {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
}
.btn-danger {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #374151;
font-weight: 600;
}
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.form-group input:focus {
outline: none;
border-color: #4f46e5;
}
.status-message {
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
font-weight: 600;
text-align: center;
}
.status-success {
background: #dcfce7;
color: #166534;
border: 1px solid #bbf7d0;
}
.status-error {
background: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
}
.attendance-list {
max-height: 400px;
overflow-y: auto;
}
.attendance-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #e2e8f0;
transition: background-color 0.3s ease;
}
.attendance-item:hover {
background: #f8fafc;
}
.attendance-item:last-child {
border-bottom: none;
}
.attendance-name {
font-weight: 600;
color: #1e293b;
}
.attendance-time {
color: #64748b;
font-size: 0.9rem;
}
.time-display {
background: #f1f5f9;
padding: 10px;
border-radius: 8px;
text-align: center;
margin-bottom: 20px;
font-family: 'Courier New', monospace;
font-size: 1.1rem;
font-weight: bold;
color: #1e293b;
}
.loading {
text-align: center;
padding: 20px;
color: #64748b;
}
.spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #4f46e5;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
gap: 20px;
padding: 20px;
}
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
padding: 20px;
}
.header h1 {
font-size: 2rem;
}
.controls {
flex-direction: column;
}
.btn {
width: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎯 Smart Attendance System</h1>
<p>Advanced Face Recognition Attendance Management</p>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number total" id="totalUsers">{{ stats.total_users }}</div>
<div class="stat-label">Total Users</div>
</div>
<div class="stat-card">
<div class="stat-number present" id="presentToday">{{ stats.present_today }}</div>
<div class="stat-label">Present Today</div>
</div>
<div class="stat-card">
<div class="stat-number absent" id="absentToday">{{ stats.absent_today }}</div>
<div class="stat-label">Absent Today</div>
</div>
</div>
<div class="main-content">
<div class="section">
<h2>πŸ“· Face Recognition</h2>
<div class="time-display" id="currentTime"></div>
<div class="camera-container">
<video id="video" autoplay muted></video>
<div class="camera-overlay"></div>
</div>
<div class="controls">
<button class="btn btn-primary" id="startCamera">Start Camera</button>
<button class="btn btn-success" id="checkIn">Check In</button>
<button class="btn btn-danger" id="checkOut">Check Out</button>
</div>
<div id="recognitionStatus"></div>
</div>
<div class="section">
<h2>πŸ‘€ Add New User</h2>
<form id="addUserForm" enctype="multipart/form-data">
<div class="form-group">
<label for="userName">Full Name</label>
<input type="text" id="userName" name="name" required placeholder="Enter full name">
</div>
<div class="form-group">
<label for="faceImage">Face Photo</label>
<input type="file" id="faceImage" name="face_image" accept="image/*" required>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">Add User</button>
</form>
<div id="addUserStatus"></div>
<h3 style="margin-top: 30px; margin-bottom: 15px; color: #1e293b;">πŸ“‹ Today's Attendance</h3>
<div class="attendance-list" id="attendanceList">
<div class="loading">
<div class="spinner"></div>
Loading attendance...
</div>
</div>
</div>
</div>
</div>
<script>
let video;
let isRecognizing = false;
// Initialize the application
document.addEventListener('DOMContentLoaded', function() {
updateCurrentTime();
setInterval(updateCurrentTime, 1000);
loadAttendanceData();
setInterval(loadAttendanceData, 30000); // Refresh every 30 seconds
setupEventListeners();
});
function updateCurrentTime() {
const now = new Date();
const options = {
timeZone: 'Asia/Kolkata',
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
};
document.getElementById('currentTime').textContent = now.toLocaleString('en-IN', options);
}
function setupEventListeners() {
document.getElementById('startCamera').addEventListener('click', startCamera);
document.getElementById('checkIn').addEventListener('click', () => recognizeAndMark('check_in'));
document.getElementById('checkOut').addEventListener('click', () => recognizeAndMark('check_out'));
document.getElementById('addUserForm').addEventListener('submit', addUser);
}
async function startCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 640 },
height: { ideal: 480 },
facingMode: 'user'
}
});
video = document.getElementById('video');
video.srcObject = stream;
document.getElementById('startCamera').textContent = 'Camera Active';
document.getElementById('startCamera').disabled = true;
showStatus('Camera started successfully!', 'success');
} catch (error) {
console.error('Error accessing camera:', error);
showStatus('Error accessing camera. Please check permissions.', 'error');
}
}
async function recognizeAndMark(type) {
if (!video || !video.srcObject || isRecognizing) {
showStatus('Please start the camera first or wait for current recognition to complete.', 'error');
return;
}
isRecognizing = true;
const actionText = type === 'check_in' ? 'Checking In' : 'Checking Out';
showStatus(`${actionText}... Please look at the camera.`, 'info');
try {
// Capture frame from video
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0);
const imageData = canvas.toDataURL('image/jpeg', 0.8);
const response = await fetch('/api/recognize', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
image: imageData,
type: type
})
});
const result = await response.json();
if (result.success) {
const actionPast = type === 'check_in' ? 'checked in' : 'checked out';
showStatus(`βœ… ${result.name} successfully ${actionPast} at ${result.time}! (Confidence: ${result.confidence}%)`, 'success');
loadAttendanceData(); // Refresh attendance list
loadStats(); // Refresh statistics
} else {
showStatus(`❌ ${result.message}`, 'error');
}
} catch (error) {
console.error('Recognition error:', error);
showStatus('Recognition failed. Please try again.', 'error');
} finally {
isRecognizing = false;
}
}
async function addUser(event) {
event.preventDefault();
const formData = new FormData();
const nameInput = document.getElementById('userName');
const imageInput = document.getElementById('faceImage');
if (!nameInput.value.trim()) {
showUserStatus('Please enter a name.', 'error');
return;
}
if (!imageInput.files[0]) {
showUserStatus('Please select a face image.', 'error');
return;
}
formData.append('name', nameInput.value.trim());
formData.append('face_image', imageInput.files[0]);
showUserStatus('Adding user... Please wait.', 'info');
try {
const response = await fetch('/api/add_user', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
showUserStatus(`βœ… ${result.message}`, 'success');
document.getElementById('addUserForm').reset();
loadStats(); // Refresh statistics
} else {
showUserStatus(`❌ ${result.message}`, 'error');
}
} catch (error) {
console.error('Add user error:', error);
showUserStatus('Failed to add user. Please try again.', 'error');
}
}
async function loadStats() {
try {
const response = await fetch('/api/stats');
const stats = await response.json();
document.getElementById('totalUsers').textContent = stats.total_users;
document.getElementById('presentToday').textContent = stats.present_today;
document.getElementById('absentToday').textContent = stats.absent_today;
} catch (error) {
console.error('Error loading stats:', error);
}
}
async function loadAttendanceData() {
try {
const response = await fetch('/api/stats');
const data = await response.json();
const attendanceList = document.getElementById('attendanceList');
if (data.today_attendance && data.today_attendance.length > 0) {
attendanceList.innerHTML = data.today_attendance.map(record => `
<div class="attendance-item">
<div>
<div class="attendance-name">${record.name}</div>
<div class="attendance-time">
${record.check_in ? `In: ${record.check_in}` : 'Not checked in'}
${record.check_out ? ` | Out: ${record.check_out}` : ''}
</div>
</div>
<div style="color: ${record.check_out ? '#10b981' : '#f59e0b'};">
${record.check_out ? 'βœ… Complete' : 'πŸ• Active'}
</div>
</div>
`).join('');
} else {
attendanceList.innerHTML = `
<div class="attendance-item" style="justify-content: center; color: #64748b;">
πŸ“ No attendance records for today
</div>
`;
}
// Update stats
document.getElementById('totalUsers').textContent = data.total_users;
document.getElementById('presentToday').textContent = data.present_today;
document.getElementById('absentToday').textContent = data.absent_today;
} catch (error) {
console.error('Error loading attendance:', error);
document.getElementById('attendanceList').innerHTML = `
<div class="attendance-item" style="justify-content: center; color: #ef4444;">
❌ Error loading attendance data
</div>
`;
}
}
function showStatus(message, type) {
const statusDiv = document.getElementById('recognitionStatus');
statusDiv.innerHTML = `<div class="status-message status-${type === 'success' ? 'success' : 'error'}">${message}</div>`;
if (type === 'success') {
setTimeout(() => {
statusDiv.innerHTML = '';
}, 5000);
}
}
function showUserStatus(message, type) {
const statusDiv = document.getElementById('addUserStatus');
statusDiv.innerHTML = `<div class="status-message status-${type === 'success' ? 'success' : 'error'}">${message}</div>`;
if (type === 'success') {
setTimeout(() => {
statusDiv.innerHTML = '';
}, 5000);
}
}
// Add visual feedback for camera status
document.getElementById('video').addEventListener('loadedmetadata', function() {
this.style.border = '3px solid #10b981';
});
// Handle page visibility changes to manage camera
document.addEventListener('visibilitychange', function() {
if (document.hidden && video && video.srcObject) {
// Optionally pause video when page is hidden
video.pause();
} else if (!document.hidden && video && video.srcObject) {
video.play();
}
});
</script>
</body>
</html>