Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Employee Attendance</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary-color: #1a3a32; | |
| --secondary-color: #2d5548; | |
| --accent-color: #4caf7d; | |
| --text-light: #f0f9f5; | |
| --text-dark: #1a3a32; | |
| } | |
| body { | |
| background-color: #f5f7f6; | |
| font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
| } | |
| .month-selector { | |
| scrollbar-width: none; | |
| -ms-overflow-style: none; | |
| } | |
| .month-selector::-webkit-scrollbar { | |
| display: none; | |
| } | |
| .attendance-chart-container { | |
| position: relative; | |
| width: 180px; | |
| height: 180px; | |
| margin: 0 auto; | |
| } | |
| .employee-card { | |
| transition: all 0.2s ease; | |
| } | |
| .employee-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.08); | |
| } | |
| .action-btn { | |
| transition: all 0.2s ease; | |
| } | |
| .action-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.1); | |
| } | |
| .selected-month { | |
| background-color: var(--primary-color); | |
| color: white; | |
| box-shadow: 0 2px 8px rgba(26, 58, 50, 0.3); | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen"> | |
| <div class="container mx-auto px-4 py-6 max-w-md"> | |
| <!-- Header --> | |
| <header class="mb-6"> | |
| <h1 class="text-2xl font-bold text-[var(--text-dark)] mb-1">Attendance Dashboard</h1> | |
| <p class="text-sm text-gray-500">Track and manage employee attendance</p> | |
| </header> | |
| <!-- Month Selector --> | |
| <div class="mb-6"> | |
| <div class="month-selector flex overflow-x-auto pb-2 gap-2 scroll-smooth" id="month-selector"> | |
| <!-- Months will be added here by JavaScript --> | |
| </div> | |
| </div> | |
| <!-- Attendance Rate Card --> | |
| <div class="bg-white rounded-xl shadow-sm p-6 mb-6 border border-gray-100"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-lg font-semibold text-[var(--text-dark)]">Attendance Overview</h2> | |
| <div class="text-xs px-2 py-1 rounded-full bg-[var(--primary-color)] text-white"> | |
| <span id="selected-month-text">June 2023</span> | |
| </div> | |
| </div> | |
| <div class="flex flex-col md:flex-row items-center"> | |
| <div class="attendance-chart-container mb-4 md:mb-0"> | |
| <canvas id="attendanceChart"></canvas> | |
| </div> | |
| <div class="text-center md:text-left md:ml-6"> | |
| <div class="text-4xl font-bold text-[var(--primary-color)] mb-2" id="attendance-rate">85%</div> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div class="text-left"> | |
| <div class="text-sm text-gray-500">Present</div> | |
| <div class="text-xl font-semibold text-[var(--accent-color)]" id="present-days">21</div> | |
| </div> | |
| <div class="text-left"> | |
| <div class="text-sm text-gray-500">Absent</div> | |
| <div class="text-xl font-semibold text-red-500" id="absent-days">4</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Action Buttons --> | |
| <div class="grid grid-cols-3 gap-3 mb-6"> | |
| <button class="action-btn bg-[var(--primary-color)] text-white rounded-xl p-3 flex flex-col items-center"> | |
| <i class="fas fa-calendar-alt text-xl mb-2"></i> | |
| <span class="text-xs font-medium">Shift Class</span> | |
| </button> | |
| <button class="action-btn bg-[var(--secondary-color)] text-white rounded-xl p-3 flex flex-col items-center"> | |
| <i class="fas fa-cog text-xl mb-2"></i> | |
| <span class="text-xs font-medium">Rules</span> | |
| </button> | |
| <button class="action-btn bg-[var(--accent-color)] text-white rounded-xl p-3 flex flex-col items-center"> | |
| <i class="fas fa-user-edit text-xl mb-2"></i> | |
| <span class="text-xs font-medium">Manual</span> | |
| </button> | |
| </div> | |
| <!-- Employee List --> | |
| <div class="bg-white rounded-xl shadow-sm overflow-hidden border border-gray-100"> | |
| <div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center"> | |
| <h2 class="text-lg font-semibold text-[var(--text-dark)]">Employee Records</h2> | |
| <div class="text-xs text-gray-500">Total: <span id="employee-count">5</span></div> | |
| </div> | |
| <div class="divide-y divide-gray-100" id="employee-list"> | |
| <!-- Employee items will be added here by JavaScript --> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Set color theme | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| primary: '#1a3a32', | |
| secondary: '#2d5548', | |
| accent: '#4caf7d', | |
| } | |
| } | |
| } | |
| } | |
| // Generate months data (current month and 5 previous months) | |
| const months = []; | |
| const currentDate = new Date(); | |
| for (let i = 5; i >= 0; i--) { | |
| const date = new Date(); | |
| date.setMonth(currentDate.getMonth() - i); | |
| months.push({ | |
| name: date.toLocaleString('default', { month: 'short' }), | |
| year: date.getFullYear(), | |
| fullName: date.toLocaleString('default', { month: 'long' }) + ' ' + date.getFullYear(), | |
| isCurrent: i === 0 | |
| }); | |
| } | |
| // Render month selector | |
| const monthSelector = document.getElementById('month-selector'); | |
| months.forEach((month, index) => { | |
| const monthElement = document.createElement('div'); | |
| monthElement.className = `flex-shrink-0 px-4 py-2 rounded-lg cursor-pointer transition-colors ${month.isCurrent ? 'selected-month' : 'bg-white text-gray-700'}`; | |
| monthElement.innerHTML = ` | |
| <div class="text-center"> | |
| <div class="font-medium">${month.name}</div> | |
| <div class="text-xs">${month.year}</div> | |
| </div> | |
| `; | |
| monthElement.addEventListener('click', () => { | |
| // Remove selected class from all months | |
| document.querySelectorAll('#month-selector > div').forEach(el => { | |
| el.classList.remove('selected-month'); | |
| el.classList.add('bg-white', 'text-gray-700'); | |
| }); | |
| // Add selected class to clicked month | |
| monthElement.classList.add('selected-month'); | |
| monthElement.classList.remove('bg-white', 'text-gray-700'); | |
| // Update selected month text | |
| document.getElementById('selected-month-text').textContent = month.fullName; | |
| // In a real app, you would fetch data for the selected month here | |
| updateAttendanceData(month); | |
| }); | |
| monthSelector.appendChild(monthElement); | |
| }); | |
| // Set initial selected month | |
| document.getElementById('selected-month-text').textContent = months.find(m => m.isCurrent).fullName; | |
| // Sample data - in a real app this would come from an API | |
| function getAttendanceData(month) { | |
| // This is just sample data - in a real app it would vary by month | |
| const basePresent = 20 + Math.floor(Math.random() * 5); | |
| const baseAbsent = 5 - Math.floor(Math.random() * 3); | |
| return { | |
| present: basePresent, | |
| absent: baseAbsent, | |
| rate: Math.round((basePresent / (basePresent + baseAbsent)) * 100) | |
| }; | |
| } | |
| // Update attendance display | |
| function updateAttendanceData(month) { | |
| const attendanceData = getAttendanceData(month); | |
| document.getElementById('attendance-rate').textContent = attendanceData.rate + '%'; | |
| document.getElementById('present-days').textContent = attendanceData.present; | |
| document.getElementById('absent-days').textContent = attendanceData.absent; | |
| // Update chart | |
| attendanceChart.data.datasets[0].data = [attendanceData.present, attendanceData.absent]; | |
| attendanceChart.update(); | |
| // Update employee list | |
| updateEmployeeList(month); | |
| } | |
| // Create pie chart | |
| const ctx = document.getElementById('attendanceChart').getContext('2d'); | |
| const attendanceChart = new Chart(ctx, { | |
| type: 'doughnut', | |
| data: { | |
| labels: ['Present', 'Absent'], | |
| datasets: [{ | |
| data: [21, 4], // Initial data | |
| backgroundColor: [ | |
| '#4caf7d', // accent color | |
| '#ef4444' // red | |
| ], | |
| borderWidth: 0, | |
| cutout: '75%' | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| display: false | |
| }, | |
| tooltip: { | |
| enabled: false | |
| } | |
| }, | |
| elements: { | |
| arc: { | |
| borderRadius: 10 | |
| } | |
| } | |
| } | |
| }); | |
| // Sample employee data | |
| function getEmployeeData(month) { | |
| // This is just sample data - in a real app it would vary by month | |
| return [ | |
| { id: 1, name: 'Zhang San', position: 'Developer', present: 20 + Math.floor(Math.random() * 3), absent: 5 - Math.floor(Math.random() * 2), avatar: 'https://randomuser.me/api/portraits/men/1.jpg' }, | |
| { id: 2, name: 'Li Si', position: 'Designer', present: 22 + Math.floor(Math.random() * 2), absent: 3 - Math.floor(Math.random() * 1), avatar: 'https://randomuser.me/api/portraits/women/2.jpg' }, | |
| { id: 3, name: 'Wang Wu', position: 'Manager', present: 18 + Math.floor(Math.random() * 4), absent: 7 - Math.floor(Math.random() * 3), avatar: 'https://randomuser.me/api/portraits/men/3.jpg' }, | |
| { id: 4, name: 'Zhao Liu', position: 'Marketing', present: 23 + Math.floor(Math.random() * 2), absent: 2 - Math.floor(Math.random() * 1), avatar: 'https://randomuser.me/api/portraits/women/4.jpg' }, | |
| { id: 5, name: 'Qian Qi', position: 'HR', present: 21 + Math.floor(Math.random() * 3), absent: 4 - Math.floor(Math.random() * 2), avatar: 'https://randomuser.me/api/portraits/men/5.jpg' } | |
| ]; | |
| } | |
| // Render employee list | |
| function updateEmployeeList(month) { | |
| const employeeList = document.getElementById('employee-list'); | |
| employeeList.innerHTML = ''; | |
| const employees = getEmployeeData(month); | |
| document.getElementById('employee-count').textContent = employees.length; | |
| employees.forEach(employee => { | |
| const rate = Math.round((employee.present / (employee.present + employee.absent)) * 100); | |
| const employeeItem = document.createElement('div'); | |
| employeeItem.className = 'employee-card px-4 py-3'; | |
| employeeItem.innerHTML = ` | |
| <div class="flex items-center"> | |
| <div class="relative mr-3"> | |
| <img src="${employee.avatar}" alt="${employee.name}" class="w-10 h-10 rounded-full object-cover"> | |
| <div class="absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-2 border-white ${rate >= 90 ? 'bg-green-500' : rate >= 70 ? 'bg-yellow-500' : 'bg-red-500'}"></div> | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <h3 class="font-medium text-[var(--text-dark)] truncate">${employee.name}</h3> | |
| <p class="text-xs text-gray-500 truncate">${employee.position}</p> | |
| </div> | |
| <div class="flex items-center space-x-3"> | |
| <div class="text-right"> | |
| <div class="text-sm font-medium ${rate >= 90 ? 'text-green-600' : rate >= 70 ? 'text-yellow-600' : 'text-red-600'}"> | |
| ${rate}% | |
| </div> | |
| <div class="text-xs text-gray-500 flex space-x-1"> | |
| <span class="text-green-500">${employee.present}P</span> | |
| <span>/</span> | |
| <span class="text-red-500">${employee.absent}A</span> | |
| </div> | |
| </div> | |
| <i class="fas fa-chevron-right text-gray-400 text-xs"></i> | |
| </div> | |
| </div> | |
| `; | |
| employeeList.appendChild(employeeItem); | |
| }); | |
| } | |
| // Initialize with current month data | |
| updateAttendanceData(months.find(m => m.isCurrent)); | |
| // Add click handlers for action buttons | |
| document.querySelectorAll('.action-btn').forEach((btn, index) => { | |
| btn.addEventListener('click', () => { | |
| const actions = ['Shift Setup', 'Rules Setup', 'Manual Attendance']; | |
| alert(`${actions[index]} clicked - this would open the ${actions[index]} screen`); | |
| }); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Tim20121221/moremore" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |