alexandre / index.html
limalex's picture
undefined - Initial Deployment
f19692c verified
# 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>
/* Custom scrollbar */
::-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;
}
/* Animation for buttons */
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.pulse:hover {
animation: pulse 1.5s infinite;
}
/* Print styles */
@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 -->
<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 Content -->
<main class="flex-grow container mx-auto px-4 py-6">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Time Entry Section -->
<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>
<!-- Records and Reports Section -->
<div class="lg:col-span-2 space-y-6">
<!-- Records Table -->
<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>
<!-- Reports Section -->
<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 -->
<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>
// Sample data storage
let timeRecords;
// Initialize storage - works differently in Electron
function initStorage() {
if (typeof localStorage !== 'undefined') {
timeRecords = JSON.parse(localStorage.getItem('timeRecords')) || [];
} else {
// Fallback for Electron
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 = [];
}
// Override localStorage methods
window.localStorage = {
getItem: () => null,
setItem: (key, value) => {
if (key === 'timeRecords') {
fs.writeFileSync(dataPath, JSON.stringify(timeRecords));
}
}
};
}
}
initStorage();
let currentEmployeeId = '';
// DOM Elements
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');
// Initialize
document.addEventListener('DOMContentLoaded', () => {
updateCurrentTime();
setInterval(updateCurrentTime, 1000);
loadTodayRecords();
loadAllRecords();
// Check for dark mode preference
if (localStorage.getItem('darkMode') === 'enabled') {
document.documentElement.classList.add('dark');
themeToggle.innerHTML = '<i class="fas fa-sun"></i>';
}
});
// Update current time
function updateCurrentTime() {
const now = new Date();
currentTimeDisplay.textContent = now.toLocaleTimeString() + ' ' + now.toLocaleDateString();
}
// Theme toggle
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>';
}
});
// Time entry buttons
clockInBtn.addEventListener('click', () => recordTime('clockIn'));
lunchOutBtn.addEventListener('click', () => recordTime('lunchOut'));
lunchInBtn.addEventListener('click', () => recordTime('lunchIn'));
clockOutBtn.addEventListener('click', () => recordTime('clockOut'));
// Record time function
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];
// Find or create today's record
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);
}
// Update the 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;
// Calculate total hours and overtime
const clockInTime = parseTimeString(record.clockIn);
const clockOutTime = parseTimeString(record.clockOut);
let totalMinutes = (clockOutTime - clockInTime) / (1000 * 60);
// Subtract lunch time if applicable
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);
// Calculate overtime (assuming 8 hours standard workday)
const overtime = Math.max(0, totalHours - 8);
record.overtime = overtime.toFixed(2);
break;
}
// Save to localStorage
localStorage.setItem('timeRecords', JSON.stringify(timeRecords));
// Update UI
loadTodayRecords();
loadAllRecords();
}
// Helper function to parse time string into Date object
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;
}
// Load today's records
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();
}
}
// Reset today's display
function resetTodayDisplay() {
todayClockIn.textContent = '--:--';
todayLunchOut.textContent = '--:--';
todayLunchIn.textContent = '--:--';
todayClockOut.textContent = '--:--';
todayTotalHours.textContent = '0.00 hrs';
todayOvertime.textContent = '0.00 hrs';
}
// Load all records based on filter
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);
// Apply additional filters
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);
}
// Sort by date (newest first)
filteredRecords.sort((a, b) => new Date(b.date) - new Date(a.date));
// Update table
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;
}
// Helper function to get week start date (Monday)
function getWeekStartDate(date) {
const day = date.getDay();
const diff = date.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is Sunday
return new Date(date.setDate(diff)).toISOString().split('T')[0];
}
// Format date for display
function formatDate(dateStr) {
const options = { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short' };
return new Date(dateStr).toLocaleDateString(undefined, options);
}
// Refresh records when filter changes
recordsFilter.addEventListener('change', loadAllRecords);
refreshRecordsBtn.addEventListener('click', loadAllRecords);
// Employee ID input change
employeeIdInput.addEventListener('change', () => {
loadTodayRecords();
loadAllRecords();
});
// Generate report
generateReportBtn.addEventListener('click', generateReport);
printReportBtn.addEventListener('click', () => window.print());
// Generate report function
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;
}
// Sort by date (oldest first for reports)
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'
}));
// Calculate summary
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';
// Group by week
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);
}
});
// Convert to array and calculate summary
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';
// Group by month
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);
}
});
// Convert to array and calculate summary
reportData = Object.values(monthlyData);
reportData.forEach(month => {
summary.totalHours += month.totalHours;
summary.overtime += month.overtime;
summary.daysWorked += month.days.length;
});
}
// Generate report HTML
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>
`;
});
}
// Add summary section
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>
`;
// Add footer
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>