|
|
|
# Time Tracker Pro - Offline Installation |
|
|
|
## Prerequisites |
|
- Node.js (v16 or later) |
|
- npm (comes with Node.js) |
|
|
|
## Installation Steps |
|
|
|
1. **Download the application files** including: |
|
- `index.html` (main application file) |
|
- `package.json` (configuration file) |
|
- `main.js` (Electron main process file) |
|
- `/build` folder with application icon (optional) |
|
|
|
2. **Install dependencies**: |
|
```bash |
|
npm install |
|
``` |
|
|
|
3. **Run the application in development mode**: |
|
```bash |
|
npm start |
|
``` |
|
|
|
4. **Create installer packages**: |
|
|
|
For Windows: |
|
```bash |
|
npm run dist |
|
``` |
|
|
|
This will create: |
|
- Setup executable in `dist` folder |
|
- Portable version in `dist/win-unpacked` |
|
|
|
## Building for Other Platforms |
|
|
|
To build for other platforms, add the appropriate build configuration to `package.json` and run: |
|
```bash |
|
npm run dist |
|
``` |
|
|
|
## Offline Usage |
|
Once installed, the application works completely offline: |
|
- All data is stored in local storage |
|
- No internet connection required |
|
- Reports can be printed or saved as PDF |
|
|
|
|
|
const { app, BrowserWindow } = require('electron') |
|
const path = require('path') |
|
|
|
function createWindow () { |
|
const win = new BrowserWindow({ |
|
width: 1200, |
|
height: 800, |
|
webPreferences: { |
|
nodeIntegration: true, |
|
contextIsolation: false |
|
} |
|
}) |
|
|
|
win.loadFile('index.html') |
|
} |
|
|
|
app.whenReady().then(() => { |
|
createWindow() |
|
|
|
app.on('activate', () => { |
|
if (BrowserWindow.getAllWindows().length === 0) { |
|
createWindow() |
|
} |
|
}) |
|
}) |
|
|
|
app.on('window-all-closed', () => { |
|
if (process.platform !== 'darwin') { |
|
app.quit() |
|
} |
|
}) |
|
|
|
|
|
{ |
|
"name": "time-tracker-pro", |
|
"version": "1.0.0", |
|
"description": "Offline Time Tracking Application", |
|
"main": "main.js", |
|
"scripts": { |
|
"start": "electron .", |
|
"pack": "electron-builder --dir", |
|
"dist": "electron-builder", |
|
"postinstall": "electron-builder install-app-deps" |
|
}, |
|
"build": { |
|
"appId": "com.example.timetrackerpro", |
|
"productName": "Time Tracker Pro", |
|
"win": { |
|
"target": "nsis", |
|
"icon": "build/icon.ico" |
|
}, |
|
"nsis": { |
|
"oneClick": false, |
|
"allowToChangeInstallationDirectory": true |
|
} |
|
}, |
|
"devDependencies": { |
|
"electron": "^25.0.0", |
|
"electron-builder": "^24.0.0" |
|
} |
|
} |
|
|
|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Time Tracker Pro</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"> |
|
<script> |
|
tailwind.config = { |
|
theme: { |
|
extend: { |
|
colors: { |
|
primary: '#3b82f6', |
|
secondary: '#10b981', |
|
dark: '#1e293b', |
|
light: '#f8fafc' |
|
} |
|
} |
|
} |
|
} |
|
</script> |
|
<style> |
|
|
|
::-webkit-scrollbar { |
|
width: 8px; |
|
height: 8px; |
|
} |
|
::-webkit-scrollbar-track { |
|
background: #f1f1f1; |
|
} |
|
::-webkit-scrollbar-thumb { |
|
background: #888; |
|
border-radius: 4px; |
|
} |
|
::-webkit-scrollbar-thumb:hover { |
|
background: #555; |
|
} |
|
|
|
|
|
@keyframes pulse { |
|
0% { transform: scale(1); } |
|
50% { transform: scale(1.05); } |
|
100% { transform: scale(1); } |
|
} |
|
.pulse:hover { |
|
animation: pulse 1.5s infinite; |
|
} |
|
|
|
|
|
@media print { |
|
.no-print { |
|
display: none !important; |
|
} |
|
.print-full { |
|
width: 100% !important; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-100 font-sans"> |
|
<div class="min-h-screen flex flex-col"> |
|
|
|
<header class="bg-dark text-white shadow-lg"> |
|
<div class="container mx-auto px-4 py-4 flex justify-between items-center"> |
|
<div class="flex items-center space-x-2"> |
|
<i class="fas fa-clock text-2xl text-primary"></i> |
|
<h1 class="text-2xl font-bold">Time Tracker Pro</h1> |
|
</div> |
|
<div class="flex items-center space-x-4"> |
|
<div id="current-time" class="text-sm bg-primary px-3 py-1 rounded-full"></div> |
|
<button id="theme-toggle" class="p-2 rounded-full hover:bg-gray-700"> |
|
<i class="fas fa-moon"></i> |
|
</button> |
|
</div> |
|
</div> |
|
</header> |
|
|
|
|
|
<main class="flex-grow container mx-auto px-4 py-6"> |
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> |
|
|
|
<div class="lg:col-span-1"> |
|
<div class="bg-white rounded-lg shadow-md p-6"> |
|
<h2 class="text-xl font-semibold mb-4 text-dark flex items-center"> |
|
<i class="fas fa-stopwatch mr-2 text-primary"></i> Time Entry |
|
</h2> |
|
|
|
<div class="space-y-4"> |
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Employee ID</label> |
|
<input type="text" id="employee-id" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"> |
|
</div> |
|
|
|
<div class="grid grid-cols-2 gap-4"> |
|
<button id="clock-in" class="bg-green-600 hover:bg-green-700 text-white py-3 px-4 rounded-md font-medium flex items-center justify-center pulse"> |
|
<i class="fas fa-sign-in-alt mr-2"></i> Clock In |
|
</button> |
|
<button id="lunch-out" class="bg-yellow-600 hover:bg-yellow-700 text-white py-3 px-4 rounded-md font-medium flex items-center justify-center pulse"> |
|
<i class="fas fa-utensils mr-2"></i> Lunch Out |
|
</button> |
|
<button id="lunch-in" class="bg-blue-600 hover:bg-blue-700 text-white py-3 px-4 rounded-md font-medium flex items-center justify-center pulse"> |
|
<i class="fas fa-utensils mr-2"></i> Lunch In |
|
</button> |
|
<button id="clock-out" class="bg-red-600 hover:bg-red-700 text-white py-3 px-4 rounded-md font-medium flex items-center justify-center pulse"> |
|
<i class="fas fa-sign-out-alt mr-2"></i> Clock Out |
|
</button> |
|
</div> |
|
|
|
<div class="pt-4 border-t border-gray-200"> |
|
<h3 class="text-lg font-medium text-dark mb-2">Today's Summary</h3> |
|
<div class="grid grid-cols-2 gap-2"> |
|
<div class="bg-gray-50 p-3 rounded"> |
|
<p class="text-xs text-gray-500">Clock In</p> |
|
<p id="today-clock-in" class="font-semibold">--:--</p> |
|
</div> |
|
<div class="bg-gray-50 p-3 rounded"> |
|
<p class="text-xs text-gray-500">Lunch Out</p> |
|
<p id="today-lunch-out" class="font-semibold">--:--</p> |
|
</div> |
|
<div class="bg-gray-50 p-3 rounded"> |
|
<p class="text-xs text-gray-500">Lunch In</p> |
|
<p id="today-lunch-in" class="font-semibold">--:--</p> |
|
</div> |
|
<div class="bg-gray-50 p-3 rounded"> |
|
<p class="text-xs text-gray-500">Clock Out</p> |
|
<p id="today-clock-out" class="font-semibold">--:--</p> |
|
</div> |
|
</div> |
|
<div class="mt-2 bg-gray-50 p-3 rounded"> |
|
<p class="text-xs text-gray-500">Total Hours</p> |
|
<p id="today-total-hours" class="font-semibold text-lg">0.00 hrs</p> |
|
</div> |
|
<div class="mt-2 bg-gray-50 p-3 rounded"> |
|
<p class="text-xs text-gray-500">Overtime</p> |
|
<p id="today-overtime" class="font-semibold text-lg">0.00 hrs</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="lg:col-span-2 space-y-6"> |
|
|
|
<div class="bg-white rounded-lg shadow-md p-6"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h2 class="text-xl font-semibold text-dark flex items-center"> |
|
<i class="fas fa-list-alt mr-2 text-primary"></i> Time Records |
|
</h2> |
|
<div class="flex space-x-2"> |
|
<select id="records-filter" class="text-sm border border-gray-300 rounded px-3 py-1 focus:outline-none focus:ring-1 focus:ring-primary"> |
|
<option value="today">Today</option> |
|
<option value="week">This Week</option> |
|
<option value="month">This Month</option> |
|
<option value="all">All Records</option> |
|
</select> |
|
<button id="refresh-records" class="p-1 text-gray-500 hover:text-primary"> |
|
<i class="fas fa-sync-alt"></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-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Clock In</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Lunch Out</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Lunch In</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Clock Out</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Overtime</th> |
|
</tr> |
|
</thead> |
|
<tbody id="records-body" class="bg-white divide-y divide-gray-200"> |
|
<tr> |
|
<td colspan="7" class="px-4 py-6 text-center text-gray-500">No records found</td> |
|
</tr> |
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow-md p-6"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h2 class="text-xl font-semibold text-dark flex items-center"> |
|
<i class="fas fa-chart-bar mr-2 text-primary"></i> Reports |
|
</h2> |
|
<div class="flex space-x-2"> |
|
<select id="report-period" class="text-sm border border-gray-300 rounded px-3 py-1 focus:outline-none focus:ring-1 focus:ring-primary"> |
|
<option value="daily">Daily</option> |
|
<option value="weekly">Weekly</option> |
|
<option value="monthly">Monthly</option> |
|
</select> |
|
<button id="generate-report" class="bg-primary hover:bg-blue-700 text-white px-4 py-1 rounded flex items-center"> |
|
<i class="fas fa-file-pdf mr-1"></i> Generate |
|
</button> |
|
<button id="print-report" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-1 rounded flex items-center no-print"> |
|
<i class="fas fa-print mr-1"></i> Print |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div id="report-content" class="mt-4"> |
|
<div class="text-center py-10 text-gray-400"> |
|
<i class="fas fa-file-alt text-4xl mb-2"></i> |
|
<p>Select report type and click Generate</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</main> |
|
|
|
|
|
<footer class="bg-dark text-white py-4"> |
|
<div class="container mx-auto px-4 text-center text-sm"> |
|
<p>© 2023 Time Tracker Pro. All rights reserved.</p> |
|
</div> |
|
</footer> |
|
</div> |
|
|
|
<script> |
|
|
|
let timeRecords; |
|
|
|
|
|
function initStorage() { |
|
if (typeof localStorage !== 'undefined') { |
|
timeRecords = JSON.parse(localStorage.getItem('timeRecords')) || []; |
|
} else { |
|
|
|
const fs = require('fs'); |
|
const path = require('path'); |
|
const dataPath = path.join(app.getPath('userData'), 'timeRecords.json'); |
|
|
|
try { |
|
timeRecords = JSON.parse(fs.readFileSync(dataPath)) || []; |
|
} catch (e) { |
|
timeRecords = []; |
|
} |
|
|
|
|
|
window.localStorage = { |
|
getItem: () => null, |
|
setItem: (key, value) => { |
|
if (key === 'timeRecords') { |
|
fs.writeFileSync(dataPath, JSON.stringify(timeRecords)); |
|
} |
|
} |
|
}; |
|
} |
|
} |
|
|
|
initStorage(); |
|
let currentEmployeeId = ''; |
|
|
|
|
|
const employeeIdInput = document.getElementById('employee-id'); |
|
const clockInBtn = document.getElementById('clock-in'); |
|
const lunchOutBtn = document.getElementById('lunch-out'); |
|
const lunchInBtn = document.getElementById('lunch-in'); |
|
const clockOutBtn = document.getElementById('clock-out'); |
|
const todayClockIn = document.getElementById('today-clock-in'); |
|
const todayLunchOut = document.getElementById('today-lunch-out'); |
|
const todayLunchIn = document.getElementById('today-lunch-in'); |
|
const todayClockOut = document.getElementById('today-clock-out'); |
|
const todayTotalHours = document.getElementById('today-total-hours'); |
|
const todayOvertime = document.getElementById('today-overtime'); |
|
const recordsFilter = document.getElementById('records-filter'); |
|
const recordsBody = document.getElementById('records-body'); |
|
const refreshRecordsBtn = document.getElementById('refresh-records'); |
|
const reportPeriod = document.getElementById('report-period'); |
|
const generateReportBtn = document.getElementById('generate-report'); |
|
const printReportBtn = document.getElementById('print-report'); |
|
const reportContent = document.getElementById('report-content'); |
|
const currentTimeDisplay = document.getElementById('current-time'); |
|
const themeToggle = document.getElementById('theme-toggle'); |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
updateCurrentTime(); |
|
setInterval(updateCurrentTime, 1000); |
|
loadTodayRecords(); |
|
loadAllRecords(); |
|
|
|
|
|
if (localStorage.getItem('darkMode') === 'enabled') { |
|
document.documentElement.classList.add('dark'); |
|
themeToggle.innerHTML = '<i class="fas fa-sun"></i>'; |
|
} |
|
}); |
|
|
|
|
|
function updateCurrentTime() { |
|
const now = new Date(); |
|
currentTimeDisplay.textContent = now.toLocaleTimeString() + ' ' + now.toLocaleDateString(); |
|
} |
|
|
|
|
|
themeToggle.addEventListener('click', () => { |
|
document.documentElement.classList.toggle('dark'); |
|
if (document.documentElement.classList.contains('dark')) { |
|
localStorage.setItem('darkMode', 'enabled'); |
|
themeToggle.innerHTML = '<i class="fas fa-sun"></i>'; |
|
} else { |
|
localStorage.setItem('darkMode', 'disabled'); |
|
themeToggle.innerHTML = '<i class="fas fa-moon"></i>'; |
|
} |
|
}); |
|
|
|
|
|
clockInBtn.addEventListener('click', () => recordTime('clockIn')); |
|
lunchOutBtn.addEventListener('click', () => recordTime('lunchOut')); |
|
lunchInBtn.addEventListener('click', () => recordTime('lunchIn')); |
|
clockOutBtn.addEventListener('click', () => recordTime('clockOut')); |
|
|
|
|
|
function recordTime(type) { |
|
currentEmployeeId = employeeIdInput.value.trim(); |
|
|
|
if (!currentEmployeeId) { |
|
alert('Please enter your Employee ID'); |
|
return; |
|
} |
|
|
|
const now = new Date(); |
|
const today = now.toISOString().split('T')[0]; |
|
|
|
|
|
let record = timeRecords.find(r => r.date === today && r.employeeId === currentEmployeeId); |
|
|
|
if (!record) { |
|
record = { |
|
date: today, |
|
employeeId: currentEmployeeId, |
|
clockIn: '', |
|
lunchOut: '', |
|
lunchIn: '', |
|
clockOut: '', |
|
totalHours: 0, |
|
overtime: 0 |
|
}; |
|
timeRecords.push(record); |
|
} |
|
|
|
|
|
const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); |
|
|
|
switch (type) { |
|
case 'clockIn': |
|
if (record.clockIn) { |
|
alert('Clock-in already recorded for today'); |
|
return; |
|
} |
|
record.clockIn = timeString; |
|
break; |
|
case 'lunchOut': |
|
if (!record.clockIn) { |
|
alert('Please clock in first'); |
|
return; |
|
} |
|
if (record.lunchOut) { |
|
alert('Lunch-out already recorded for today'); |
|
return; |
|
} |
|
record.lunchOut = timeString; |
|
break; |
|
case 'lunchIn': |
|
if (!record.lunchOut) { |
|
alert('Please take lunch out first'); |
|
return; |
|
} |
|
if (record.lunchIn) { |
|
alert('Lunch-in already recorded for today'); |
|
return; |
|
} |
|
record.lunchIn = timeString; |
|
break; |
|
case 'clockOut': |
|
if (!record.clockIn) { |
|
alert('Please clock in first'); |
|
return; |
|
} |
|
if (record.clockOut) { |
|
alert('Clock-out already recorded for today'); |
|
return; |
|
} |
|
record.clockOut = timeString; |
|
|
|
|
|
const clockInTime = parseTimeString(record.clockIn); |
|
const clockOutTime = parseTimeString(record.clockOut); |
|
|
|
let totalMinutes = (clockOutTime - clockInTime) / (1000 * 60); |
|
|
|
|
|
if (record.lunchOut && record.lunchIn) { |
|
const lunchOutTime = parseTimeString(record.lunchOut); |
|
const lunchInTime = parseTimeString(record.lunchIn); |
|
const lunchMinutes = (lunchInTime - lunchOutTime) / (1000 * 60); |
|
totalMinutes -= lunchMinutes; |
|
} |
|
|
|
const totalHours = totalMinutes / 60; |
|
record.totalHours = totalHours.toFixed(2); |
|
|
|
|
|
const overtime = Math.max(0, totalHours - 8); |
|
record.overtime = overtime.toFixed(2); |
|
break; |
|
} |
|
|
|
|
|
localStorage.setItem('timeRecords', JSON.stringify(timeRecords)); |
|
|
|
|
|
loadTodayRecords(); |
|
loadAllRecords(); |
|
} |
|
|
|
|
|
function parseTimeString(timeStr) { |
|
const [time, period] = timeStr.split(' '); |
|
const [hours, minutes] = time.split(':').map(Number); |
|
|
|
let hours24 = hours; |
|
if (period === 'PM' && hours < 12) hours24 += 12; |
|
if (period === 'AM' && hours === 12) hours24 = 0; |
|
|
|
const date = new Date(); |
|
date.setHours(hours24, minutes, 0, 0); |
|
return date; |
|
} |
|
|
|
|
|
function loadTodayRecords() { |
|
const today = new Date().toISOString().split('T')[0]; |
|
const employeeId = employeeIdInput.value.trim(); |
|
|
|
if (!employeeId) { |
|
resetTodayDisplay(); |
|
return; |
|
} |
|
|
|
const record = timeRecords.find(r => r.date === today && r.employeeId === employeeId); |
|
|
|
if (record) { |
|
todayClockIn.textContent = record.clockIn || '--:--'; |
|
todayLunchOut.textContent = record.lunchOut || '--:--'; |
|
todayLunchIn.textContent = record.lunchIn || '--:--'; |
|
todayClockOut.textContent = record.clockOut || '--:--'; |
|
todayTotalHours.textContent = record.totalHours ? record.totalHours + ' hrs' : '0.00 hrs'; |
|
todayOvertime.textContent = record.overtime ? record.overtime + ' hrs' : '0.00 hrs'; |
|
} else { |
|
resetTodayDisplay(); |
|
} |
|
} |
|
|
|
|
|
function resetTodayDisplay() { |
|
todayClockIn.textContent = '--:--'; |
|
todayLunchOut.textContent = '--:--'; |
|
todayLunchIn.textContent = '--:--'; |
|
todayClockOut.textContent = '--:--'; |
|
todayTotalHours.textContent = '0.00 hrs'; |
|
todayOvertime.textContent = '0.00 hrs'; |
|
} |
|
|
|
|
|
function loadAllRecords() { |
|
const filter = recordsFilter.value; |
|
const employeeId = employeeIdInput.value.trim(); |
|
|
|
if (!employeeId) { |
|
recordsBody.innerHTML = '<tr><td colspan="7" class="px-4 py-6 text-center text-gray-500">Please enter Employee ID</td></tr>'; |
|
return; |
|
} |
|
|
|
let filteredRecords = timeRecords.filter(r => r.employeeId === employeeId); |
|
|
|
|
|
const now = new Date(); |
|
const today = now.toISOString().split('T')[0]; |
|
const weekStart = getWeekStartDate(now); |
|
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split('T')[0]; |
|
|
|
if (filter === 'today') { |
|
filteredRecords = filteredRecords.filter(r => r.date === today); |
|
} else if (filter === 'week') { |
|
filteredRecords = filteredRecords.filter(r => r.date >= weekStart); |
|
} else if (filter === 'month') { |
|
filteredRecords = filteredRecords.filter(r => r.date >= monthStart); |
|
} |
|
|
|
|
|
filteredRecords.sort((a, b) => new Date(b.date) - new Date(a.date)); |
|
|
|
|
|
if (filteredRecords.length === 0) { |
|
recordsBody.innerHTML = '<tr><td colspan="7" class="px-4 py-6 text-center text-gray-500">No records found</td></tr>'; |
|
return; |
|
} |
|
|
|
let html = ''; |
|
filteredRecords.forEach(record => { |
|
html += ` |
|
<tr> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${formatDate(record.date)}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${record.clockIn || '--:--'}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${record.lunchOut || '--:--'}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${record.lunchIn || '--:--'}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${record.clockOut || '--:--'}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900 font-medium">${record.totalHours || '0.00'} hrs</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900 font-medium">${record.overtime || '0.00'} hrs</td> |
|
</tr> |
|
`; |
|
}); |
|
|
|
recordsBody.innerHTML = html; |
|
} |
|
|
|
|
|
function getWeekStartDate(date) { |
|
const day = date.getDay(); |
|
const diff = date.getDate() - day + (day === 0 ? -6 : 1); |
|
return new Date(date.setDate(diff)).toISOString().split('T')[0]; |
|
} |
|
|
|
|
|
function formatDate(dateStr) { |
|
const options = { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short' }; |
|
return new Date(dateStr).toLocaleDateString(undefined, options); |
|
} |
|
|
|
|
|
recordsFilter.addEventListener('change', loadAllRecords); |
|
refreshRecordsBtn.addEventListener('click', loadAllRecords); |
|
|
|
|
|
employeeIdInput.addEventListener('change', () => { |
|
loadTodayRecords(); |
|
loadAllRecords(); |
|
}); |
|
|
|
|
|
generateReportBtn.addEventListener('click', generateReport); |
|
printReportBtn.addEventListener('click', () => window.print()); |
|
|
|
|
|
function generateReport() { |
|
const period = reportPeriod.value; |
|
const employeeId = employeeIdInput.value.trim(); |
|
|
|
if (!employeeId) { |
|
reportContent.innerHTML = ` |
|
<div class="text-center py-10 text-red-500"> |
|
<i class="fas fa-exclamation-circle text-4xl mb-2"></i> |
|
<p>Please enter Employee ID</p> |
|
</div> |
|
`; |
|
return; |
|
} |
|
|
|
let filteredRecords = timeRecords.filter(r => r.employeeId === employeeId); |
|
|
|
if (filteredRecords.length === 0) { |
|
reportContent.innerHTML = ` |
|
<div class="text-center py-10 text-gray-500"> |
|
<i class="fas fa-file-alt text-4xl mb-2"></i> |
|
<p>No records found for this employee</p> |
|
</div> |
|
`; |
|
return; |
|
} |
|
|
|
|
|
filteredRecords.sort((a, b) => new Date(a.date) - new Date(b.date)); |
|
|
|
let reportTitle = ''; |
|
let reportData = []; |
|
let summary = { totalHours: 0, overtime: 0, daysWorked: 0 }; |
|
|
|
if (period === 'daily') { |
|
reportTitle = 'Daily Time Report'; |
|
reportData = filteredRecords.map(record => ({ |
|
date: formatDate(record.date), |
|
clockIn: record.clockIn || '--:--', |
|
lunchOut: record.lunchOut || '--:--', |
|
lunchIn: record.lunchIn || '--:--', |
|
clockOut: record.clockOut || '--:--', |
|
totalHours: record.totalHours || '0.00', |
|
overtime: record.overtime || '0.00' |
|
})); |
|
|
|
|
|
filteredRecords.forEach(record => { |
|
if (record.totalHours) { |
|
summary.totalHours += parseFloat(record.totalHours); |
|
summary.overtime += parseFloat(record.overtime); |
|
summary.daysWorked++; |
|
} |
|
}); |
|
} else if (period === 'weekly') { |
|
reportTitle = 'Weekly Time Report'; |
|
|
|
|
|
const weeklyData = {}; |
|
filteredRecords.forEach(record => { |
|
const date = new Date(record.date); |
|
const weekStart = getWeekStartDate(date); |
|
const weekEnd = new Date(new Date(weekStart).getTime() + 6 * 24 * 60 * 60 * 1000); |
|
const weekLabel = `${formatDate(weekStart)} to ${formatDate(weekEnd.toISOString().split('T')[0])}`; |
|
|
|
if (!weeklyData[weekLabel]) { |
|
weeklyData[weekLabel] = { |
|
week: weekLabel, |
|
days: [], |
|
totalHours: 0, |
|
overtime: 0 |
|
}; |
|
} |
|
|
|
weeklyData[weekLabel].days.push({ |
|
date: formatDate(record.date), |
|
clockIn: record.clockIn || '--:--', |
|
lunchOut: record.lunchOut || '--:--', |
|
lunchIn: record.lunchIn || '--:--', |
|
clockOut: record.clockOut || '--:--', |
|
totalHours: record.totalHours || '0.00', |
|
overtime: record.overtime || '0.00' |
|
}); |
|
|
|
if (record.totalHours) { |
|
weeklyData[weekLabel].totalHours += parseFloat(record.totalHours); |
|
weeklyData[weekLabel].overtime += parseFloat(record.overtime); |
|
} |
|
}); |
|
|
|
|
|
reportData = Object.values(weeklyData); |
|
reportData.forEach(week => { |
|
summary.totalHours += week.totalHours; |
|
summary.overtime += week.overtime; |
|
summary.daysWorked += week.days.length; |
|
}); |
|
} else if (period === 'monthly') { |
|
reportTitle = 'Monthly Time Report'; |
|
|
|
|
|
const monthlyData = {}; |
|
filteredRecords.forEach(record => { |
|
const date = new Date(record.date); |
|
const monthLabel = date.toLocaleDateString(undefined, { month: 'long', year: 'numeric' }); |
|
|
|
if (!monthlyData[monthLabel]) { |
|
monthlyData[monthLabel] = { |
|
month: monthLabel, |
|
days: [], |
|
totalHours: 0, |
|
overtime: 0 |
|
}; |
|
} |
|
|
|
monthlyData[monthLabel].days.push({ |
|
date: formatDate(record.date), |
|
clockIn: record.clockIn || '--:--', |
|
lunchOut: record.lunchOut || '--:--', |
|
lunchIn: record.lunchIn || '--:--', |
|
clockOut: record.clockOut || '--:--', |
|
totalHours: record.totalHours || '0.00', |
|
overtime: record.overtime || '0.00' |
|
}); |
|
|
|
if (record.totalHours) { |
|
monthlyData[monthLabel].totalHours += parseFloat(record.totalHours); |
|
monthlyData[monthLabel].overtime += parseFloat(record.overtime); |
|
} |
|
}); |
|
|
|
|
|
reportData = Object.values(monthlyData); |
|
reportData.forEach(month => { |
|
summary.totalHours += month.totalHours; |
|
summary.overtime += month.overtime; |
|
summary.daysWorked += month.days.length; |
|
}); |
|
} |
|
|
|
|
|
let reportHTML = ` |
|
<div class="print-full"> |
|
<div class="flex justify-between items-center mb-6 border-b pb-4"> |
|
<div> |
|
<h3 class="text-2xl font-bold text-dark">${reportTitle}</h3> |
|
<p class="text-gray-600">Employee ID: ${employeeId}</p> |
|
<p class="text-gray-600">Generated on: ${new Date().toLocaleDateString()}</p> |
|
</div> |
|
<div class="bg-primary text-white p-2 rounded"> |
|
<i class="fas fa-clock text-3xl"></i> |
|
</div> |
|
</div> |
|
`; |
|
|
|
if (period === 'daily') { |
|
reportHTML += ` |
|
<div class="overflow-x-auto"> |
|
<table class="min-w-full divide-y divide-gray-200"> |
|
<thead class="bg-gray-50"> |
|
<tr> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Clock In</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Lunch Out</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Lunch In</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Clock Out</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total Hours</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Overtime</th> |
|
</tr> |
|
</thead> |
|
<tbody class="bg-white divide-y divide-gray-200"> |
|
`; |
|
|
|
reportData.forEach(record => { |
|
reportHTML += ` |
|
<tr> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${record.date}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${record.clockIn}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${record.lunchOut}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${record.lunchIn}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${record.clockOut}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900 font-medium">${record.totalHours} hrs</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900 font-medium">${record.overtime} hrs</td> |
|
</tr> |
|
`; |
|
}); |
|
|
|
reportHTML += ` |
|
</tbody> |
|
</table> |
|
</div> |
|
`; |
|
} else if (period === 'weekly') { |
|
reportData.forEach(week => { |
|
reportHTML += ` |
|
<div class="mb-8"> |
|
<h4 class="text-lg font-semibold mb-2 bg-gray-100 p-2 rounded">${week.week}</h4> |
|
<div class="overflow-x-auto mb-2"> |
|
<table class="min-w-full divide-y divide-gray-200"> |
|
<thead class="bg-gray-50"> |
|
<tr> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Clock In</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Lunch Out</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Lunch In</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Clock Out</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total Hours</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Overtime</th> |
|
</tr> |
|
</thead> |
|
<tbody class="bg-white divide-y divide-gray-200"> |
|
`; |
|
|
|
week.days.forEach(day => { |
|
reportHTML += ` |
|
<tr> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${day.date}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${day.clockIn}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${day.lunchOut}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${day.lunchIn}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${day.clockOut}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900 font-medium">${day.totalHours} hrs</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900 font-medium">${day.overtime} hrs</td> |
|
</tr> |
|
`; |
|
}); |
|
|
|
reportHTML += ` |
|
</tbody> |
|
</table> |
|
</div> |
|
<div class="flex justify-end"> |
|
<div class="bg-gray-100 p-3 rounded"> |
|
<p class="text-sm font-medium">Week Total: <span class="font-bold">${week.totalHours.toFixed(2)} hrs</span></p> |
|
<p class="text-sm font-medium">Week Overtime: <span class="font-bold">${week.overtime.toFixed(2)} hrs</span></p> |
|
</div> |
|
</div> |
|
</div> |
|
`; |
|
}); |
|
} else if (period === 'monthly') { |
|
reportData.forEach(month => { |
|
reportHTML += ` |
|
<div class="mb-8"> |
|
<h4 class="text-lg font-semibold mb-2 bg-gray-100 p-2 rounded">${month.month}</h4> |
|
<div class="overflow-x-auto mb-2"> |
|
<table class="min-w-full divide-y divide-gray-200"> |
|
<thead class="bg-gray-50"> |
|
<tr> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Clock In</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Lunch Out</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Lunch In</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Clock Out</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total Hours</th> |
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Overtime</th> |
|
</tr> |
|
</thead> |
|
<tbody class="bg-white divide-y divide-gray-200"> |
|
`; |
|
|
|
month.days.forEach(day => { |
|
reportHTML += ` |
|
<tr> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${day.date}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${day.clockIn}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${day.lunchOut}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${day.lunchIn}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">${day.clockOut}</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900 font-medium">${day.totalHours} hrs</td> |
|
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900 font-medium">${day.overtime} hrs</td> |
|
</tr> |
|
`; |
|
}); |
|
|
|
reportHTML += ` |
|
</tbody> |
|
</table> |
|
</div> |
|
<div class="flex justify-end"> |
|
<div class="bg-gray-100 p-3 rounded"> |
|
<p class="text-sm font-medium">Month Total: <span class="font-bold">${month.totalHours.toFixed(2)} hrs</span></p> |
|
<p class="text-sm font-medium">Month Overtime: <span class="font-bold">${month.overtime.toFixed(2)} hrs</span></p> |
|
</div> |
|
</div> |
|
</div> |
|
`; |
|
}); |
|
} |
|
|
|
|
|
reportHTML += ` |
|
<div class="mt-8 pt-4 border-t border-gray-200"> |
|
<h4 class="text-lg font-semibold mb-4">Summary</h4> |
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> |
|
<div class="bg-blue-50 p-4 rounded-lg"> |
|
<p class="text-sm text-blue-600">Days Worked</p> |
|
<p class="text-2xl font-bold text-blue-800">${summary.daysWorked}</p> |
|
</div> |
|
<div class="bg-green-50 p-4 rounded-lg"> |
|
<p class="text-sm text-green-600">Total Hours</p> |
|
<p class="text-2xl font-bold text-green-800">${summary.totalHours.toFixed(2)} hrs</p> |
|
</div> |
|
<div class="bg-purple-50 p-4 rounded-lg"> |
|
<p class="text-sm text-purple-600">Total Overtime</p> |
|
<p class="text-2xl font-bold text-purple-800">${summary.overtime.toFixed(2)} hrs</p> |
|
</div> |
|
</div> |
|
</div> |
|
`; |
|
|
|
|
|
reportHTML += ` |
|
<div class="mt-8 pt-4 border-t border-gray-200 text-center text-xs text-gray-500"> |
|
<p>This report was generated by Time Tracker Pro on ${new Date().toLocaleString()}</p> |
|
</div> |
|
</div> |
|
`; |
|
|
|
reportContent.innerHTML = reportHTML; |
|
} |
|
</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=limalex/alexandre" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |