| <!DOCTYPE html> |
| <html lang="ru"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>WorkTime Bot</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| body { |
| font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; |
| background-color: var(--tg-theme-bg-color, #f7f8fc); |
| color: var(--tg-theme-text-color, #000000); |
| } |
| |
| .header { |
| background-color: var(--tg-theme-header-bg-color, #0088cc); |
| color: var(--tg-theme-header-text-color, #ffffff); |
| } |
| |
| .button-primary { |
| background-color: var(--tg-theme-button-color, #0088cc); |
| color: var(--tg-theme-button-text-color, #ffffff); |
| } |
| |
| .button-secondary { |
| background-color: var(--tg-theme-secondary-bg-color, #ebedf0); |
| color: var(--tg-theme-text-color, #000000); |
| } |
| |
| .card { |
| background-color: var(--tg-theme-bg-color, #ffffff); |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); |
| } |
| |
| .progress-bar { |
| height: 6px; |
| border-radius: 3px; |
| background-color: var(--tg-theme-secondary-bg-color, #ebedf0); |
| } |
| |
| .progress-fill { |
| height: 100%; |
| border-radius: 3px; |
| background-color: var(--tg-theme-button-color, #0088cc); |
| transition: width 0.3s ease; |
| } |
| |
| .pulse { |
| animation: pulse 2s infinite; |
| } |
| |
| @keyframes pulse { |
| 0% { |
| box-shadow: 0 0 0 0 rgba(0, 136, 204, 0.7); |
| } |
| 70% { |
| box-shadow: 0 0 0 10px rgba(0, 136, 204, 0); |
| } |
| 100% { |
| box-shadow: 0 0 0 0 rgba(0, 136, 204, 0); |
| } |
| } |
| |
| .fade-in { |
| animation: fadeIn 0.3s ease-in; |
| } |
| |
| @keyframes fadeIn { |
| from { opacity: 0; transform: translateY(10px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| .telegram-input { |
| border: 1px solid var(--tg-theme-secondary-bg-color, #ebedf0); |
| background-color: var(--tg-theme-bg-color, #ffffff); |
| color: var(--tg-theme-text-color, #000000); |
| } |
| |
| .telegram-hint { |
| color: var(--tg-theme-hint-color, #707579); |
| } |
| </style> |
| </head> |
| <body class="min-h-screen"> |
| <div class="header px-4 py-3 flex items-center"> |
| <button id="back-button" class="mr-3"> |
| <i class="fas fa-chevron-left text-white"></i> |
| </button> |
| <h1 class="text-lg font-semibold">WorkTime Bot</h1> |
| </div> |
| |
| <div class="container mx-auto px-4 py-3"> |
| |
| <div id="main-screen"> |
| <div class="card rounded-xl p-4 mb-3"> |
| <div class="flex justify-between items-center mb-3"> |
| <div> |
| <h2 class="font-medium">Текущий статус</h2> |
| <p class="text-sm telegram-hint" id="current-date">Сегодня, 12 июня</p> |
| </div> |
| <div class="px-3 py-1 rounded-full bg-blue-100 text-blue-800 text-sm" id="current-status"> |
| Не начат |
| </div> |
| </div> |
| |
| <div class="grid grid-cols-2 gap-2 mb-3"> |
| <button id="start-btn" class="button-primary pulse rounded-lg py-3 px-4 font-medium flex items-center justify-center"> |
| <i class="fas fa-play mr-2"></i> Старт |
| </button> |
| <button id="end-btn" class="button-secondary rounded-lg py-3 px-4 font-medium flex items-center justify-center" disabled> |
| <i class="fas fa-stop mr-2"></i> Стоп |
| </button> |
| </div> |
| |
| <div class="mb-3"> |
| <div class="flex justify-between mb-1"> |
| <span class="text-sm telegram-hint">Прогресс дня</span> |
| <span class="text-sm font-medium" id="work-progress">0%</span> |
| </div> |
| <div class="progress-bar"> |
| <div class="progress-fill" id="progress-fill" style="width: 0%"></div> |
| </div> |
| </div> |
| |
| <div class="grid grid-cols-3 gap-2 text-center"> |
| <div class="rounded-lg p-2 bg-blue-50"> |
| <p class="text-xs telegram-hint">Сегодня</p> |
| <p class="font-bold" id="today-hours">0 ч</p> |
| </div> |
| <div class="rounded-lg p-2 bg-blue-50"> |
| <p class="text-xs telegram-hint">Дней</p> |
| <p class="font-bold" id="worked-days">0</p> |
| </div> |
| <div class="rounded-lg p-2 bg-blue-50"> |
| <p class="text-xs telegram-hint">Всего</p> |
| <p class="font-bold" id="total-hours">0 ч</p> |
| </div> |
| </div> |
| </div> |
| |
| <div class="card rounded-xl p-4 mb-3"> |
| <div class="flex justify-between items-center mb-3"> |
| <h2 class="font-medium">Статистика</h2> |
| <div class="flex items-center"> |
| <button id="prev-month" class="p-1 rounded-full hover:bg-gray-100"> |
| <i class="fas fa-chevron-left telegram-hint"></i> |
| </button> |
| <span class="mx-2 text-sm font-medium" id="current-month">Июнь 2023</span> |
| <button id="next-month" class="p-1 rounded-full hover:bg-gray-100"> |
| <i class="fas fa-chevron-right telegram-hint"></i> |
| </button> |
| </div> |
| </div> |
| |
| <div class="overflow-hidden rounded-lg"> |
| <table class="min-w-full"> |
| <thead class="bg-gray-100"> |
| <tr> |
| <th class="px-3 py-2 text-left text-xs font-medium telegram-hint">Дата</th> |
| <th class="px-3 py-2 text-left text-xs font-medium telegram-hint">Часы</th> |
| </tr> |
| </thead> |
| <tbody class="divide-y divide-gray-200" id="month-table-body"> |
| |
| </tbody> |
| </table> |
| </div> |
| |
| <div class="mt-3 pt-2 border-t border-gray-200 text-right"> |
| <p class="text-xs telegram-hint">Итого за месяц:</p> |
| <p class="text-sm font-medium" id="month-total">0 дней, 0 часов</p> |
| </div> |
| </div> |
| |
| <div class="card rounded-xl p-4"> |
| <h2 class="font-medium mb-3">История</h2> |
| <div class="space-y-2" id="history-list"> |
| |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="settings-screen" class="hidden"> |
| <div class="card rounded-xl p-4 mb-3"> |
| <h2 class="font-medium mb-3">Настройки</h2> |
| |
| <div class="mb-3"> |
| <label class="block text-sm font-medium mb-1">Рабочих часов в день</label> |
| <input type="number" id="work-hours-per-day" class="telegram-input w-full rounded-lg px-3 py-2" value="8" min="1" max="24"> |
| </div> |
| |
| <div class="mb-3"> |
| <label class="block text-sm font-medium mb-1">Уведомления</label> |
| <div class="flex items-center justify-between"> |
| <span class="text-sm">Напоминать о перерывах</span> |
| <label class="relative inline-flex items-center cursor-pointer"> |
| <input type="checkbox" id="break-reminders" class="sr-only peer"> |
| <div class="w-9 h-5 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"></div> |
| </label> |
| </div> |
| </div> |
| |
| <button id="save-settings" class="button-primary w-full rounded-lg py-3 px-4 font-medium"> |
| Сохранить |
| </button> |
| </div> |
| |
| <div class="card rounded-xl p-4"> |
| <h2 class="font-medium mb-3">О боте</h2> |
| <p class="text-sm mb-2">WorkTime Bot v1.0</p> |
| <p class="text-sm telegram-hint">Отслеживайте свое рабочее время прямо в Telegram</p> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 flex justify-around py-2"> |
| <button id="main-tab" class="flex flex-col items-center px-4 py-1 text-blue-600"> |
| <i class="fas fa-home"></i> |
| <span class="text-xs mt-1">Главная</span> |
| </button> |
| <button id="stats-tab" class="flex flex-col items-center px-4 py-1 text-gray-500"> |
| <i class="fas fa-chart-bar"></i> |
| <span class="text-xs mt-1">Статистика</span> |
| </button> |
| <button id="settings-tab" class="flex flex-col items-center px-4 py-1 text-gray-500"> |
| <i class="fas fa-cog"></i> |
| <span class="text-xs mt-1">Настройки</span> |
| </button> |
| </div> |
|
|
| <script> |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| if (!localStorage.getItem('workTimeBot')) { |
| localStorage.setItem('workTimeBot', JSON.stringify({ |
| currentSession: null, |
| workDays: {}, |
| monthlyStats: {}, |
| settings: { |
| workHoursPerDay: 8, |
| breakReminders: true |
| } |
| })); |
| } |
| |
| const appData = JSON.parse(localStorage.getItem('workTimeBot')); |
| |
| |
| const startBtn = document.getElementById('start-btn'); |
| const endBtn = document.getElementById('end-btn'); |
| const currentStatus = document.getElementById('current-status'); |
| const todayHours = document.getElementById('today-hours'); |
| const workedDays = document.getElementById('worked-days'); |
| const totalHours = document.getElementById('total-hours'); |
| const progressFill = document.getElementById('progress-fill'); |
| const workProgress = document.getElementById('work-progress'); |
| const currentDate = document.getElementById('current-date'); |
| const currentMonth = document.getElementById('current-month'); |
| const monthTableBody = document.getElementById('month-table-body'); |
| const monthTotal = document.getElementById('month-total'); |
| const historyList = document.getElementById('history-list'); |
| const prevMonthBtn = document.getElementById('prev-month'); |
| const nextMonthBtn = document.getElementById('next-month'); |
| const mainScreen = document.getElementById('main-screen'); |
| const settingsScreen = document.getElementById('settings-screen'); |
| const mainTab = document.getElementById('main-tab'); |
| const settingsTab = document.getElementById('settings-tab'); |
| const workHoursInput = document.getElementById('work-hours-per-day'); |
| const breakRemindersToggle = document.getElementById('break-reminders'); |
| const saveSettingsBtn = document.getElementById('save-settings'); |
| const backButton = document.getElementById('back-button'); |
| |
| |
| const now = new Date(); |
| const today = formatDate(now); |
| currentDate.textContent = `Сегодня, ${formatDate(now, true)}`; |
| |
| |
| let currentMonthView = new Date(now.getFullYear(), now.getMonth(), 1); |
| updateMonthView(); |
| |
| |
| workHoursInput.value = appData.settings.workHoursPerDay; |
| breakRemindersToggle.checked = appData.settings.breakReminders; |
| |
| |
| if (appData.currentSession) { |
| const sessionStart = new Date(appData.currentSession.startTime); |
| if (formatDate(sessionStart) === today) { |
| |
| startBtn.disabled = true; |
| endBtn.disabled = false; |
| currentStatus.textContent = 'Работаю'; |
| startBtn.classList.remove('pulse'); |
| |
| |
| updateTimer(); |
| const timerInterval = setInterval(updateTimer, 1000); |
| |
| endBtn.addEventListener('click', function() { |
| clearInterval(timerInterval); |
| endWorkSession(); |
| }); |
| } else { |
| |
| endWorkSession(appData.currentSession.startTime, new Date(appData.currentSession.startTime).setHours(23, 59, 59, 999)); |
| } |
| } |
| |
| |
| startBtn.addEventListener('click', startWorkSession); |
| |
| |
| prevMonthBtn.addEventListener('click', function() { |
| currentMonthView.setMonth(currentMonthView.getMonth() - 1); |
| updateMonthView(); |
| }); |
| |
| nextMonthBtn.addEventListener('click', function() { |
| currentMonthView.setMonth(currentMonthView.getMonth() + 1); |
| updateMonthView(); |
| }); |
| |
| |
| settingsTab.addEventListener('click', function() { |
| mainScreen.classList.add('hidden'); |
| settingsScreen.classList.remove('hidden'); |
| updateTabStyles('settings'); |
| }); |
| |
| mainTab.addEventListener('click', function() { |
| mainScreen.classList.remove('hidden'); |
| settingsScreen.classList.add('hidden'); |
| updateTabStyles('main'); |
| }); |
| |
| backButton.addEventListener('click', function() { |
| |
| alert('В реальном Telegram боте это закрыло бы веб-приложение'); |
| }); |
| |
| |
| saveSettingsBtn.addEventListener('click', function() { |
| appData.settings.workHoursPerDay = parseInt(workHoursInput.value) || 8; |
| appData.settings.breakReminders = breakRemindersToggle.checked; |
| localStorage.setItem('workTimeBot', JSON.stringify(appData)); |
| |
| |
| mainScreen.classList.remove('hidden'); |
| settingsScreen.classList.add('hidden'); |
| updateTabStyles('main'); |
| |
| |
| showTelegramNotification('Настройки сохранены'); |
| }); |
| |
| |
| updateStats(); |
| |
| |
| function formatDate(date, short = false) { |
| if (short) { |
| return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long' }); |
| } |
| return date.toLocaleDateString('ru-RU'); |
| } |
| |
| function formatTime(date) { |
| return date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); |
| } |
| |
| function startWorkSession() { |
| const startTime = new Date(); |
| |
| appData.currentSession = { |
| startTime: startTime.toISOString() |
| }; |
| |
| localStorage.setItem('workTimeBot', JSON.stringify(appData)); |
| |
| startBtn.disabled = true; |
| endBtn.disabled = false; |
| currentStatus.textContent = 'Работаю'; |
| startBtn.classList.remove('pulse'); |
| |
| |
| addHistoryEvent('Начало работы', startTime); |
| |
| |
| updateTimer(); |
| const timerInterval = setInterval(updateTimer, 1000); |
| |
| endBtn.addEventListener('click', function() { |
| clearInterval(timerInterval); |
| endWorkSession(); |
| }); |
| |
| |
| showTelegramNotification('Рабочий день начат'); |
| } |
| |
| function endWorkSession(customStartTime, customEndTime) { |
| const endTime = customEndTime ? new Date(customEndTime) : new Date(); |
| const startTime = customStartTime ? new Date(customStartTime) : new Date(appData.currentSession.startTime); |
| |
| const workDate = formatDate(startTime); |
| const durationMs = endTime - startTime; |
| const durationHours = (durationMs / (1000 * 60 * 60)).toFixed(2); |
| |
| |
| if (!appData.workDays[workDate]) { |
| appData.workDays[workDate] = { |
| startTime: startTime.toISOString(), |
| endTime: endTime.toISOString(), |
| duration: durationHours |
| }; |
| |
| |
| const monthKey = `${startTime.getFullYear()}-${String(startTime.getMonth() + 1).padStart(2, '0')}`; |
| if (!appData.monthlyStats[monthKey]) { |
| appData.monthlyStats[monthKey] = { |
| days: 0, |
| hours: 0 |
| }; |
| } |
| |
| appData.monthlyStats[monthKey].days += 1; |
| appData.monthlyStats[monthKey].hours += parseFloat(durationHours); |
| } |
| |
| |
| appData.currentSession = null; |
| localStorage.setItem('workTimeBot', JSON.stringify(appData)); |
| |
| startBtn.disabled = false; |
| endBtn.disabled = true; |
| currentStatus.textContent = 'Не начат'; |
| startBtn.classList.add('pulse'); |
| |
| |
| progressFill.style.width = '0%'; |
| workProgress.textContent = '0%'; |
| todayHours.textContent = '0 ч'; |
| |
| |
| addHistoryEvent('Конец работы', endTime, durationHours); |
| |
| |
| updateStats(); |
| updateMonthView(); |
| |
| |
| showTelegramNotification(`Рабочий день завершен: ${durationHours} ч`); |
| } |
| |
| function updateTimer() { |
| const startTime = new Date(appData.currentSession.startTime); |
| const now = new Date(); |
| const durationMs = now - startTime; |
| const durationHours = (durationMs / (1000 * 60 * 60)).toFixed(2); |
| |
| |
| todayHours.textContent = `${durationHours} ч`; |
| |
| |
| const workHoursPerDay = appData.settings.workHoursPerDay || 8; |
| const progress = Math.min((durationMs / (workHoursPerDay * 60 * 60 * 1000)) * 100, 100); |
| progressFill.style.width = `${progress}%`; |
| workProgress.textContent = `${Math.round(progress)}%`; |
| } |
| |
| function updateStats() { |
| |
| let totalDays = 0; |
| let totalHours = 0; |
| |
| for (const month in appData.monthlyStats) { |
| totalDays += appData.monthlyStats[month].days; |
| totalHours += appData.monthlyStats[month].hours; |
| } |
| |
| workedDays.textContent = totalDays; |
| totalHours.textContent = `${totalHours.toFixed(1)} ч`; |
| } |
| |
| function updateMonthView() { |
| const monthName = currentMonthView.toLocaleDateString('ru-RU', { month: 'long', year: 'numeric' }); |
| currentMonth.textContent = monthName.charAt(0).toUpperCase() + monthName.slice(1); |
| |
| const monthKey = `${currentMonthView.getFullYear()}-${String(currentMonthView.getMonth() + 1).padStart(2, '0')}`; |
| const monthStats = appData.monthlyStats[monthKey] || { days: 0, hours: 0 }; |
| |
| |
| monthTableBody.innerHTML = ''; |
| |
| |
| const monthWorkDays = []; |
| for (const date in appData.workDays) { |
| const workDay = appData.workDays[date]; |
| const workDate = new Date(workDay.startTime); |
| |
| if (workDate.getFullYear() === currentMonthView.getFullYear() && |
| workDate.getMonth() === currentMonthView.getMonth()) { |
| monthWorkDays.push({ |
| date: workDate, |
| startTime: new Date(workDay.startTime), |
| endTime: new Date(workDay.endTime), |
| duration: workDay.duration |
| }); |
| } |
| } |
| |
| |
| monthWorkDays.sort((a, b) => b.date - a.date); |
| |
| |
| monthWorkDays.forEach(day => { |
| const row = document.createElement('tr'); |
| row.className = 'fade-in'; |
| |
| row.innerHTML = ` |
| <td class="px-3 py-2 text-sm">${formatDate(day.date)}</td> |
| <td class="px-3 py-2 text-sm font-medium">${day.duration} ч</td> |
| `; |
| |
| monthTableBody.appendChild(row); |
| }); |
| |
| |
| if (monthWorkDays.length === 0) { |
| const row = document.createElement('tr'); |
| row.innerHTML = ` |
| <td colspan="2" class="px-3 py-4 text-center text-sm telegram-hint">Нет данных за этот месяц</td> |
| `; |
| monthTableBody.appendChild(row); |
| } |
| |
| |
| monthTotal.textContent = `${monthStats.days} дней, ${monthStats.hours.toFixed(1)} часов`; |
| } |
| |
| function addHistoryEvent(event, time, duration = null) { |
| const eventElement = document.createElement('div'); |
| eventElement.className = 'fade-in p-3 rounded-lg bg-gray-100'; |
| |
| let eventText = ` |
| <div class="flex justify-between items-center"> |
| <div> |
| <p class="font-medium">${event}</p> |
| <p class="text-xs telegram-hint">${formatTime(time)}</p> |
| </div> |
| `; |
| |
| if (duration) { |
| eventText += ` |
| <span class="bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full"> |
| ${duration} ч |
| </span> |
| `; |
| } |
| |
| eventText += `</div>`; |
| eventElement.innerHTML = eventText; |
| |
| |
| if (historyList.firstChild) { |
| historyList.insertBefore(eventElement, historyList.firstChild); |
| } else { |
| historyList.appendChild(eventElement); |
| } |
| |
| |
| if (historyList.children.length > 10) { |
| historyList.removeChild(historyList.lastChild); |
| } |
| } |
| |
| function updateTabStyles(activeTab) { |
| const tabs = ['main', 'stats', 'settings']; |
| tabs.forEach(tab => { |
| const tabElement = document.getElementById(`${tab}-tab`); |
| if (tab === activeTab) { |
| tabElement.classList.remove('text-gray-500'); |
| tabElement.classList.add('text-blue-600'); |
| } else { |
| tabElement.classList.remove('text-blue-600'); |
| tabElement.classList.add('text-gray-500'); |
| } |
| }); |
| } |
| |
| function showTelegramNotification(message) { |
| |
| console.log('Telegram notification:', message); |
| |
| |
| const notification = document.createElement('div'); |
| notification.className = 'fixed top-4 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg text-sm fade-in'; |
| notification.textContent = message; |
| document.body.appendChild(notification); |
| |
| setTimeout(() => { |
| notification.classList.add('opacity-0', 'transition-opacity', 'duration-300'); |
| setTimeout(() => notification.remove(), 300); |
| }, 3000); |
| } |
| |
| |
| function loadHistory() { |
| historyList.innerHTML = ''; |
| |
| |
| const events = []; |
| |
| for (const date in appData.workDays) { |
| const workDay = appData.workDays[date]; |
| events.push({ |
| type: 'start', |
| time: new Date(workDay.startTime), |
| duration: null |
| }); |
| |
| events.push({ |
| type: 'end', |
| time: new Date(workDay.endTime), |
| duration: workDay.duration |
| }); |
| } |
| |
| |
| events.sort((a, b) => b.time - a.time); |
| |
| |
| events.slice(0, 10).forEach(event => { |
| addHistoryEvent( |
| event.type === 'start' ? 'Начало работы' : 'Конец работы', |
| event.time, |
| event.duration |
| ); |
| }); |
| } |
| |
| loadHistory(); |
| updateTabStyles('main'); |
| }); |
| </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=AliDu14/work-time" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |