|
<!DOCTYPE html> |
|
<html lang="ru"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Учет рабочего времени</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> |
|
.progress-bar { |
|
height: 8px; |
|
border-radius: 4px; |
|
background-color: #e5e7eb; |
|
overflow: hidden; |
|
} |
|
|
|
.progress-fill { |
|
height: 100%; |
|
border-radius: 4px; |
|
background-color: #3b82f6; |
|
transition: width 0.3s ease; |
|
} |
|
|
|
.floating-btn { |
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.floating-btn:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); |
|
} |
|
|
|
.floating-btn:active { |
|
transform: translateY(0); |
|
} |
|
|
|
.pulse { |
|
animation: pulse 2s infinite; |
|
} |
|
|
|
@keyframes pulse { |
|
0% { |
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); |
|
} |
|
70% { |
|
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); |
|
} |
|
100% { |
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); |
|
} |
|
} |
|
|
|
.fade-in { |
|
animation: fadeIn 0.5s ease-in; |
|
} |
|
|
|
@keyframes fadeIn { |
|
from { opacity: 0; transform: translateY(10px); } |
|
to { opacity: 1; transform: translateY(0); } |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-50 min-h-screen"> |
|
<div class="container mx-auto px-4 py-8 max-w-3xl"> |
|
<header class="text-center mb-8"> |
|
<h1 class="text-3xl font-bold text-gray-800 mb-2">Учет рабочего времени</h1> |
|
<p class="text-gray-600">Отслеживайте свои рабочие часы и дни</p> |
|
</header> |
|
|
|
<div class="bg-white rounded-xl shadow-md p-6 mb-8"> |
|
<div class="flex justify-between items-center mb-6"> |
|
<div> |
|
<h2 class="text-xl font-semibold text-gray-800">Текущий статус</h2> |
|
<p class="text-gray-500 text-sm" id="current-date">Сегодня: </p> |
|
</div> |
|
<div class="bg-blue-50 text-blue-800 px-4 py-2 rounded-lg"> |
|
<span class="font-medium" id="current-status">Не начат</span> |
|
</div> |
|
</div> |
|
|
|
<div class="grid grid-cols-2 gap-4 mb-6"> |
|
<button id="start-btn" class="floating-btn pulse bg-green-500 hover:bg-green-600 text-white py-4 px-6 rounded-lg font-medium flex items-center justify-center"> |
|
<i class="fas fa-play mr-2"></i> Начало дня |
|
</button> |
|
<button id="end-btn" class="floating-btn bg-red-500 hover:bg-red-600 text-white py-4 px-6 rounded-lg font-medium flex items-center justify-center" disabled> |
|
<i class="fas fa-stop mr-2"></i> Конец дня |
|
</button> |
|
</div> |
|
|
|
<div class="mb-6"> |
|
<div class="flex justify-between mb-2"> |
|
<span class="text-sm font-medium text-gray-700">Прогресс рабочего дня</span> |
|
<span class="text-sm font-medium text-gray-700" 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-4 text-center"> |
|
<div class="bg-gray-50 p-4 rounded-lg"> |
|
<p class="text-sm text-gray-500">Отработано сегодня</p> |
|
<p class="text-2xl font-bold text-gray-800" id="today-hours">0 ч</p> |
|
</div> |
|
<div class="bg-gray-50 p-4 rounded-lg"> |
|
<p class="text-sm text-gray-500">Отработано дней</p> |
|
<p class="text-2xl font-bold text-gray-800" id="worked-days">0</p> |
|
</div> |
|
<div class="bg-gray-50 p-4 rounded-lg"> |
|
<p class="text-sm text-gray-500">Всего часов</p> |
|
<p class="text-2xl font-bold text-gray-800" id="total-hours">0 ч</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="bg-white rounded-xl shadow-md p-6 mb-8"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h2 class="text-xl font-semibold text-gray-800">Статистика за месяц</h2> |
|
<div class="flex items-center"> |
|
<button id="prev-month" class="p-2 rounded-full hover:bg-gray-100"> |
|
<i class="fas fa-chevron-left text-gray-600"></i> |
|
</button> |
|
<span class="mx-4 font-medium" id="current-month">Июнь 2023</span> |
|
<button id="next-month" class="p-2 rounded-full hover:bg-gray-100"> |
|
<i class="fas fa-chevron-right text-gray-600"></i> |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<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 class="bg-white divide-y divide-gray-200" id="month-table-body"> |
|
|
|
</tbody> |
|
</table> |
|
</div> |
|
|
|
<div class="mt-4 text-right"> |
|
<p class="text-sm text-gray-500">Итого за месяц:</p> |
|
<p class="text-lg font-semibold" id="month-total">0 рабочих дней, 0 часов</p> |
|
</div> |
|
</div> |
|
|
|
<div class="bg-white rounded-xl shadow-md p-6"> |
|
<h2 class="text-xl font-semibold text-gray-800 mb-4">История</h2> |
|
<div class="space-y-4" id="history-list"> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
if (!localStorage.getItem('workTimeTracker')) { |
|
localStorage.setItem('workTimeTracker', JSON.stringify({ |
|
currentSession: null, |
|
workDays: {}, |
|
monthlyStats: {} |
|
})); |
|
} |
|
|
|
const appData = JSON.parse(localStorage.getItem('workTimeTracker')); |
|
|
|
|
|
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 now = new Date(); |
|
const today = formatDate(now); |
|
currentDate.textContent = `Сегодня: ${formatDate(now, true)}`; |
|
|
|
|
|
let currentMonthView = new Date(now.getFullYear(), now.getMonth(), 1); |
|
updateMonthView(); |
|
|
|
|
|
if (appData.currentSession) { |
|
const sessionStart = new Date(appData.currentSession.startTime); |
|
if (formatDate(sessionStart) === today) { |
|
|
|
startBtn.disabled = true; |
|
endBtn.disabled = false; |
|
currentStatus.textContent = 'Работаю'; |
|
document.getElementById('start-btn').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(); |
|
}); |
|
|
|
|
|
updateStats(); |
|
|
|
|
|
function formatDate(date, withWeekday = false) { |
|
const options = { year: 'numeric', month: '2-digit', day: '2-digit' }; |
|
if (withWeekday) { |
|
options.weekday = 'long'; |
|
} |
|
return date.toLocaleDateString('ru-RU', options); |
|
} |
|
|
|
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('workTimeTracker', JSON.stringify(appData)); |
|
|
|
startBtn.disabled = true; |
|
endBtn.disabled = false; |
|
currentStatus.textContent = 'Работаю'; |
|
document.getElementById('start-btn').classList.remove('pulse'); |
|
|
|
|
|
addHistoryEvent('Начало рабочего дня', startTime); |
|
|
|
|
|
updateTimer(); |
|
const timerInterval = setInterval(updateTimer, 1000); |
|
|
|
endBtn.addEventListener('click', function() { |
|
clearInterval(timerInterval); |
|
endWorkSession(); |
|
}); |
|
} |
|
|
|
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('workTimeTracker', JSON.stringify(appData)); |
|
|
|
startBtn.disabled = false; |
|
endBtn.disabled = true; |
|
currentStatus.textContent = 'Не начат'; |
|
document.getElementById('start-btn').classList.add('pulse'); |
|
|
|
|
|
progressFill.style.width = '0%'; |
|
workProgress.textContent = '0%'; |
|
todayHours.textContent = '0 ч'; |
|
|
|
|
|
addHistoryEvent('Конец рабочего дня', endTime, durationHours); |
|
|
|
|
|
updateStats(); |
|
updateMonthView(); |
|
} |
|
|
|
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 progress = Math.min((durationMs / (8 * 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-6 py-4 whitespace-nowrap text-sm text-gray-900">${formatDate(day.date)}</td> |
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formatTime(day.startTime)}</td> |
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formatTime(day.endTime)}</td> |
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${day.duration} ч</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 bg-gray-50 p-4 rounded-lg'; |
|
|
|
let eventText = ` |
|
<div class="flex justify-between items-center"> |
|
<div> |
|
<p class="font-medium text-gray-800">${event}</p> |
|
<p class="text-sm text-gray-500">${formatTime(time)}</p> |
|
</div> |
|
`; |
|
|
|
if (duration) { |
|
eventText += ` |
|
<span class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 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 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(); |
|
}); |
|
</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> |