Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>TimeTrack Pro RU</title> | |
| <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> | |
| <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <style> | |
| body { | |
| font-family: 'Roboto', sans-serif; | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| button, input, select, textarea { | |
| font-size: 16px ; /* Prevent zoom on focus in mobile browsers */ | |
| } | |
| </style> | |
| <style> | |
| .time-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); | |
| } | |
| .pulse-animation { | |
| animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <div id="app" class="min-h-screen"> | |
| <!-- Header --> | |
| <header class="bg-indigo-600 text-white shadow-lg"> | |
| <div class="container mx-auto px-4 py-6"> | |
| <div class="flex justify-between items-center"> | |
| <div class="flex items-center space-x-3"> | |
| <i data-feather="clock" class="w-8 h-8"></i> | |
| <h1 class="text-2xl font-bold">ТаймТрек Про RU</h1> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button class="px-4 py-2 bg-indigo-700 rounded-lg hover:bg-indigo-800 transition flex items-center"> | |
| <i data-feather="user" class="mr-2"></i> | |
| Профиль | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="container mx-auto px-4 py-8"> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <!-- Time Tracking Card --> | |
| <div class="lg:col-span-2"> | |
| <div class="bg-white rounded-xl shadow-md p-6"> | |
| <h2 class="text-xl font-semibold mb-6 flex items-center"> | |
| <i data-feather="watch" class="mr-2 text-indigo-600"></i> | |
| Текущая смена | |
| </h2> | |
| <div class="flex flex-col items-center justify-center py-8"> | |
| <div class="text-5xl font-bold mb-4" id="current-time">--:--:--</div> | |
| <div class="text-gray-500 mb-8" id="current-date">Loading date...</div> | |
| <div class="flex space-x-4 mb-8"> | |
| <button id="clock-in-btn" class="px-6 py-3 bg-green-500 text-white rounded-lg hover:bg-green-600 transition flex items-center pulse-animation"> | |
| <i data-feather="log-in" class="mr-2"></i> | |
| Начать смену | |
| </button> | |
| <button id="clock-out-btn" disabled class="px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600 transition flex items-center opacity-50"> | |
| <i data-feather="log-out" class="mr-2"></i> | |
| Закончить смену | |
| </button> | |
| <div id="comment-section" class="w-full mt-4 hidden"> | |
| <textarea id="shift-comment" placeholder="Добавьте комментарий к смене" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"></textarea> | |
| <button id="save-comment" class="mt-2 px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition"> | |
| Сохранить комментарий | |
| </button> | |
| </div> | |
| </div> | |
| <div class="w-full bg-gray-200 rounded-full h-2.5 mb-4"> | |
| <div id="progress-bar" class="bg-indigo-600 h-2.5 rounded-full" style="width: 0%"></div> | |
| </div> | |
| <p class="text-sm text-gray-600">Прогресс смены: <span id="progress-text">0%</span></p> | |
| </div> | |
| </div> | |
| <!-- Recent Shifts --> | |
| <div class="bg-white rounded-xl shadow-md p-6 mt-6"> | |
| <h2 class="text-xl font-semibold mb-6 flex items-center"> | |
| <i data-feather="list" class="mr-2 text-indigo-600"></i> | |
| Последние смены | |
| </h2> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Дата</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Начало</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Конец</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Часы</th> | |
| </tr> | |
| </thead> | |
| <tbody id="shifts-table" class="bg-white divide-y divide-gray-200"> | |
| <!-- Data will be inserted here by JavaScript --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Stats and Summary --> | |
| <div class="flex flex-col space-y-6"> | |
| <!-- Today's Summary --> | |
| <div class="bg-white rounded-xl shadow-md p-6"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i data-feather="bar-chart-2" class="mr-2 text-indigo-600"></i> | |
| Сегодняшний отчет | |
| </h2> | |
| <div class="space-y-4"> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-gray-600">Всего времени:</span> | |
| <span id="today-hours" class="font-bold">0h 0m</span> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-gray-600">Заработок (₽):</span> | |
| <span id="today-earnings" class="font-bold">$0.00</span> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-gray-600">Статус:</span> | |
| <span id="today-status" class="font-bold text-red-500">Не на смене</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Weekly Summary --> | |
| <div class="bg-white rounded-xl shadow-md p-6"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i data-feather="calendar" class="mr-2 text-indigo-600"></i> | |
| Недельный отчет | |
| </h2> | |
| <div class="space-y-4"> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-gray-600">Всего часов:</span> | |
| <span id="weekly-hours" class="font-bold">0h 0m</span> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-gray-600">Ожидаемая оплата (₽):</span> | |
| <span id="weekly-earnings" class="font-bold">$0.00</span> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-gray-600">Отработано дней:</span> | |
| <span id="weekly-days" class="font-bold">0</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Settings Card --> | |
| <div class="bg-white rounded-xl shadow-md p-6"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i data-feather="settings" class="mr-2 text-indigo-600"></i> | |
| Настройки | |
| </h2> | |
| <div class="space-y-4"> | |
| <div> | |
| <label for="hourly-rate" class="block text-sm font-medium text-gray-700 mb-1">Ставка (₽/час)</label> | |
| <input type="number" id="hourly-rate" value="15.00" | |
| class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| </div> | |
| <div> | |
| <label for="work-hours" class="block text-sm font-medium text-gray-700 mb-1">Часов в день</label> | |
| <input type="number" id="work-hours" value="8" | |
| class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
| </div> | |
| <button id="save-settings" class="w-full px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition"> | |
| Сохранить | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Footer --> | |
| <footer class="bg-gray-800 text-white py-6 mt-12"> | |
| <div class="container mx-auto px-4 text-center"> | |
| <p>© 2023 ТаймТрек Про RU. Все права защищены.</p> | |
| <p class="text-gray-400 text-sm mt-2">Учитывайте время, увеличивайте заработок!</p> | |
| </div> | |
| </footer> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| feather.replace(); | |
| // Update current time every second | |
| function updateClock() { | |
| const now = new Date(); | |
| const timeElement = document.getElementById('current-time'); | |
| const dateElement = document.getElementById('current-date'); | |
| timeElement.textContent = now.toLocaleTimeString(); | |
| dateElement.textContent = now.toLocaleDateString('ru-RU', { | |
| weekday: 'long', | |
| year: 'numeric', | |
| month: 'long', | |
| day: 'numeric' | |
| }); | |
| } | |
| updateClock(); | |
| setInterval(updateClock, 1000); | |
| // Sample data for shifts (in a real app, this would come from localStorage or a database) | |
| let shifts = JSON.parse(localStorage.getItem('shifts')) || []; | |
| let shiftComments = JSON.parse(localStorage.getItem('shiftComments')) || {}; | |
| let isClockedIn = false; | |
| let currentShift = null; | |
| // Check if there's an active shift | |
| if (shifts.length > 0 && !shifts[shifts.length - 1].clockOut) { | |
| isClockedIn = true; | |
| currentShift = shifts[shifts.length - 1]; | |
| updateClockInUI(true); | |
| } | |
| // Populate shifts table | |
| function populateShiftsTable() { | |
| const tableBody = document.getElementById('shifts-table'); | |
| tableBody.innerHTML = ''; | |
| // Show only last 5 shifts for simplicity | |
| const recentShifts = shifts.slice(-5).reverse(); | |
| recentShifts.forEach(shift => { | |
| const row = document.createElement('tr'); | |
| const dateCell = document.createElement('td'); | |
| dateCell.className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-900'; | |
| dateCell.textContent = new Date(shift.clockIn).toLocaleDateString(); | |
| const inCell = document.createElement('td'); | |
| inCell.className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-500'; | |
| inCell.textContent = new Date(shift.clockIn).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); | |
| const outCell = document.createElement('td'); | |
| outCell.className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-500'; | |
| outCell.textContent = shift.clockOut ? | |
| new Date(shift.clockOut).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) : '--:--'; | |
| const hoursCell = document.createElement('td'); | |
| hoursCell.className = 'px-6 py-4 whitespace-nowrap text-sm font-medium'; | |
| if (shift.clockOut) { | |
| const hours = (new Date(shift.clockOut) - new Date(shift.clockIn)) / (1000 * 60 * 60); | |
| hoursCell.textContent = hours.toFixed(2) + 'h'; | |
| hoursCell.classList.add('text-green-600'); | |
| } else { | |
| hoursCell.textContent = 'Active'; | |
| hoursCell.classList.add('text-indigo-600'); | |
| } | |
| row.appendChild(dateCell); | |
| row.appendChild(inCell); | |
| row.appendChild(outCell); | |
| row.appendChild(hoursCell); | |
| tableBody.appendChild(row); | |
| addCommentToRow(row, shift); | |
| }); | |
| updateSummaryCards(); | |
| } | |
| // Update summary cards with calculations | |
| function updateSummaryCards() { | |
| // Today's hours | |
| const today = new Date().toLocaleDateString(); | |
| const todayShifts = shifts.filter(shift => | |
| new Date(shift.clockIn).toLocaleDateString() === today && shift.clockOut | |
| ); | |
| let todayMinutes = 0; | |
| todayShifts.forEach(shift => { | |
| todayMinutes += (new Date(shift.clockOut) - new Date(shift.clockIn)) / (1000 * 60); | |
| }); | |
| const todayHours = Math.floor(todayMinutes / 60); | |
| const todayMins = Math.floor(todayMinutes % 60); | |
| document.getElementById('today-hours').textContent = `${todayHours}h ${todayMins}m`; | |
| // Weekly hours (last 7 days) | |
| const oneWeekAgo = new Date(); | |
| oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); | |
| const weeklyShifts = shifts.filter(shift => | |
| new Date(shift.clockIn) >= oneWeekAgo && shift.clockOut | |
| ); | |
| let weeklyMinutes = 0; | |
| const uniqueDays = new Set(); | |
| weeklyShifts.forEach(shift => { | |
| weeklyMinutes += (new Date(shift.clockOut) - new Date(shift.clockIn)) / (1000 * 60); | |
| uniqueDays.add(new Date(shift.clockIn).toLocaleDateString()); | |
| }); | |
| const weeklyHours = Math.floor(weeklyMinutes / 60); | |
| const weeklyMins = Math.floor(weeklyMinutes % 60); | |
| document.getElementById('weekly-hours').textContent = `${weeklyHours}h ${weeklyMins}m`; | |
| document.getElementById('weekly-days').textContent = uniqueDays.size; | |
| // Earnings calculations | |
| const hourlyRate = parseFloat(document.getElementById('hourly-rate').value) || 15.00; | |
| document.getElementById('today-earnings').textContent = `${(todayMinutes / 60 * hourlyRate).toFixed(2)}₽`; | |
| document.getElementById('weekly-earnings').textContent = `${(weeklyMinutes / 60 * hourlyRate).toFixed(2)}₽`; | |
| // Update status | |
| const statusElement = document.getElementById('today-status'); | |
| if (isClockedIn) { | |
| statusElement.textContent = 'На смене'; | |
| statusElement.classList.remove('text-red-500'); | |
| statusElement.classList.add('text-green-500'); | |
| } else { | |
| statusElement.textContent = 'Не на смене'; | |
| statusElement.classList.remove('text-green-500'); | |
| statusElement.classList.add('text-red-500'); | |
| } | |
| } | |
| // Update UI when clocked in/out | |
| function updateClockInUI(clockedIn) { | |
| const clockInBtn = document.getElementById('clock-in-btn'); | |
| const clockOutBtn = document.getElementById('clock-out-btn'); | |
| if (clockedIn) { | |
| clockInBtn.disabled = true; | |
| clockInBtn.classList.remove('bg-green-500', 'hover:bg-green-600', 'pulse-animation'); | |
| clockInBtn.classList.add('bg-gray-400', 'hover:bg-gray-500'); | |
| clockOutBtn.disabled = false; | |
| clockOutBtn.classList.remove('opacity-50'); | |
| // Start tracking shift progress | |
| updateShiftProgress(); | |
| setInterval(updateShiftProgress, 60000); // Update every minute | |
| } else { | |
| clockInBtn.disabled = false; | |
| clockInBtn.classList.add('bg-green-500', 'hover:bg-green-600', 'pulse-animation'); | |
| clockInBtn.classList.remove('bg-gray-400', 'hover:bg-gray-500'); | |
| clockOutBtn.disabled = true; | |
| clockOutBtn.classList.add('opacity-50'); | |
| } | |
| } | |
| // Update progress bar for current shift | |
| function updateShiftProgress() { | |
| if (!isClockedIn) return; | |
| const workHours = parseFloat(document.getElementById('work-hours').value) || 8; | |
| const clockInTime = new Date(currentShift.clockIn); | |
| const now = new Date(); | |
| const elapsedHours = (now - clockInTime) / (1000 * 60 * 60); | |
| const progressPercent = Math.min(100, (elapsedHours / workHours) * 100); | |
| document.getElementById('progress-bar').style.width = `${progressPercent}%`; | |
| document.getElementById('progress-text').textContent = `${progressPercent.toFixed(1)}%`; | |
| // Change color based on progress | |
| const progressBar = document.getElementById('progress-bar'); | |
| progressBar.classList.remove('bg-red-500', 'bg-yellow-500', 'bg-green-500'); | |
| if (progressPercent < 50) { | |
| progressBar.classList.add('bg-red-500'); | |
| } else if (progressPercent < 90) { | |
| progressBar.classList.add('bg-yellow-500'); | |
| } else { | |
| progressBar.classList.add('bg-green-500'); | |
| } | |
| } | |
| // Clock In button click | |
| document.getElementById('clock-in-btn').addEventListener('click', function() { | |
| const now = new Date(); | |
| currentShift = { | |
| clockIn: now.toISOString(), | |
| clockOut: null, | |
| comment: '' | |
| }; | |
| shifts.push(currentShift); | |
| localStorage.setItem('shifts', JSON.stringify(shifts)); | |
| document.getElementById('comment-section').classList.add('hidden'); | |
| isClockedIn = true; | |
| updateClockInUI(true); | |
| // Add comment display to shift rows | |
| function addCommentToRow(row, shift) { | |
| if (shift.comment) { | |
| const commentRow = document.createElement('tr'); | |
| commentRow.className = 'bg-gray-50'; | |
| const commentCell = document.createElement('td'); | |
| commentCell.colSpan = 4; | |
| commentCell.className = 'px-6 py-4 text-sm text-gray-500'; | |
| commentCell.textContent = `Комментарий: ${shift.comment}`; | |
| commentRow.appendChild(commentCell); | |
| row.parentNode.insertBefore(commentRow, row.nextSibling); | |
| } | |
| } | |
| function populateShiftsTable() { | |
| }); | |
| // Clock Out button click | |
| document.getElementById('clock-out-btn').addEventListener('click', function() { | |
| document.getElementById('comment-section').classList.remove('hidden'); | |
| document.getElementById('clock-out-btn').disabled = true; | |
| }); | |
| document.getElementById('save-comment').addEventListener('click', function() { | |
| const comment = document.getElementById('shift-comment').value; | |
| if (currentShift) { | |
| currentShift.comment = comment; | |
| shiftComments[currentShift.clockIn] = comment; | |
| localStorage.setItem('shiftComments', JSON.stringify(shiftComments)); | |
| } | |
| if (!isClockedIn || !currentShift) return; | |
| const now = new Date(); | |
| currentShift.clockOut = now.toISOString(); | |
| // Update in shifts array | |
| const index = shifts.findIndex(s => s.clockIn === currentShift.clockIn && !s.clockOut); | |
| if (index !== -1) { | |
| shifts[index] = currentShift; | |
| } | |
| localStorage.setItem('shifts', JSON.stringify(shifts)); | |
| isClockedIn = false; | |
| currentShift = null; | |
| updateClockInUI(false); | |
| populateShiftsTable(); | |
| }); | |
| // Save settings | |
| document.getElementById('save-settings').addEventListener('click', function() { | |
| const hourlyRate = document.getElementById('hourly-rate').value; | |
| const workHours = document.getElementById('work-hours').value; | |
| localStorage.setItem('hourlyRate', hourlyRate); | |
| localStorage.setItem('workHours', workHours); | |
| updateSummaryCards(); | |
| // Show confirmation | |
| const button = this; | |
| button.textContent = 'Сохранено!'; | |
| setTimeout(() => { | |
| button.textContent = 'Сохранить'; | |
| }, 2000); | |
| }); | |
| // Load saved settings | |
| if (localStorage.getItem('hourlyRate')) { | |
| document.getElementById('hourly-rate').value = localStorage.getItem('hourlyRate'); | |
| } | |
| if (localStorage.getItem('workHours')) { | |
| document.getElementById('work-hours').value = localStorage.getItem('workHours'); | |
| } | |
| // Initialize the UI | |
| populateShiftsTable(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |