testing-space / index.html
Khulekanis's picture
Add 2 files
9f7335c verified
Raw
History Blame Contribute Delete
59.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TimeTrack Pro - Complete Time Tracking</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>
.time-entry:hover .entry-actions {
opacity: 1;
}
.timer-circle {
width: 12px;
height: 12px;
border-radius: 50%;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { transform: scale(0.95); opacity: 0.7; }
50% { transform: scale(1.1); opacity: 1; }
100% { transform: scale(0.95); opacity: 0.7; }
}
.modal {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.modal-overlay {
background-color: rgba(0, 0, 0, 0.5);
}
.dropdown-content {
display: none;
position: absolute;
right: 0;
background-color: white;
min-width: 200px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.1);
z-index: 1;
border-radius: 0.5rem;
}
.dropdown:hover .dropdown-content {
display: block;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>
</head>
<body class="bg-gray-50 font-sans">
<div id="app" class="flex h-screen overflow-hidden">
<!-- Sidebar -->
<div class="sidebar bg-white text-gray-700 w-64 flex flex-col border-r border-gray-200">
<div class="p-4 flex items-center justify-between border-b border-gray-200">
<div class="flex items-center">
<i class="fas fa-clock text-blue-500 text-2xl mr-3"></i>
<span class="sidebar-text font-bold text-xl">TimeTrack Pro</span>
</div>
<button @click="toggleSidebar" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-bars"></i>
</button>
</div>
<div class="flex-1 overflow-y-auto">
<div class="p-4">
<button @click="showProjectModal = true" class="w-full bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded flex items-center justify-center">
<i class="fas fa-plus mr-2 sidebar-text"></i>
<span class="sidebar-text">New Project</span>
</button>
</div>
<div class="px-4">
<div @click="activeView = 'dashboard'" class="flex items-center mb-2 cursor-pointer" :class="{'text-blue-500': activeView === 'dashboard', 'text-gray-500 hover:text-blue-500': activeView !== 'dashboard'}">
<i class="fas fa-home mr-3"></i>
<span class="sidebar-text">Dashboard</span>
</div>
<div @click="activeView = 'tracker'" class="flex items-center mb-2 cursor-pointer" :class="{'text-blue-500': activeView === 'tracker', 'text-gray-500 hover:text-blue-500': activeView !== 'tracker'}">
<i class="fas fa-clock mr-3"></i>
<span class="sidebar-text">Time Tracker</span>
</div>
<div @click="activeView = 'reports'" class="flex items-center mb-2 cursor-pointer" :class="{'text-blue-500': activeView === 'reports', 'text-gray-500 hover:text-blue-500': activeView !== 'reports'}">
<i class="fas fa-calendar-alt mr-3"></i>
<span class="sidebar-text">Reports</span>
</div>
<div @click="activeView = 'projects'" class="flex items-center mb-2 cursor-pointer" :class="{'text-blue-500': activeView === 'projects', 'text-gray-500 hover:text-blue-500': activeView !== 'projects'}">
<i class="fas fa-project-diagram mr-3"></i>
<span class="sidebar-text">Projects</span>
</div>
</div>
<div class="px-4 mt-8">
<p class="text-xs text-gray-400 uppercase mb-2 sidebar-text">Projects</p>
<div v-for="project in projects" :key="project.id"
@click="selectProject(project.id)"
class="flex items-center mb-2 cursor-pointer"
:class="{'text-blue-500': selectedProject === project.id, 'text-gray-500 hover:text-blue-500': selectedProject !== project.id}">
<div class="w-3 h-3 rounded-full mr-3" :style="{backgroundColor: project.color}"></div>
<span class="sidebar-text">{{ project.name }}</span>
</div>
</div>
</div>
<div class="p-4 border-t border-gray-200">
<div class="flex items-center">
<div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-bold mr-2">JD</div>
<div class="sidebar-text">
<p class="text-sm font-medium">John Doe</p>
<p class="text-xs text-gray-500">john@example.com</p>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="main-content flex-1 flex flex-col overflow-hidden">
<!-- Top Navigation -->
<div class="bg-white border-b border-gray-200 py-4 px-6 flex items-center justify-between">
<div class="flex items-center">
<h1 class="text-xl font-semibold">{{ getActiveViewTitle() }}</h1>
</div>
<div class="flex items-center space-x-4">
<button class="text-gray-500 hover:text-gray-700">
<i class="fas fa-bell"></i>
</button>
<div class="relative dropdown">
<button class="flex items-center text-gray-700 hover:text-gray-900">
<div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-bold mr-2">JD</div>
<span>John Doe</span>
<i class="fas fa-chevron-down ml-2 text-xs"></i>
</button>
<div class="dropdown-content mt-2 py-2">
<a href="#" class="block px-4 py-2 text-gray-700 hover:bg-gray-100">Profile</a>
<a href="#" class="block px-4 py-2 text-gray-700 hover:bg-gray-100">Settings</a>
<a href="#" class="block px-4 py-2 text-gray-700 hover:bg-gray-100">Logout</a>
</div>
</div>
</div>
</div>
<!-- Time Tracker View -->
<div v-if="activeView === 'tracker'" class="flex-1 overflow-y-auto">
<!-- Timer Section -->
<div class="bg-white mx-6 my-6 rounded-lg shadow-sm p-6">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center">
<div class="timer-circle mr-3" :class="{'bg-red-500': timerRunning, 'bg-gray-400': !timerRunning}"></div>
<h2 class="text-lg font-semibold">{{ timerRunning ? 'Currently tracking time' : 'Timer stopped' }}</h2>
</div>
<div class="flex items-center space-x-2">
<div class="relative">
<button @click="showProjectDropdown = !showProjectDropdown" class="bg-gray-100 hover:bg-gray-200 text-gray-700 py-2 px-4 rounded flex items-center">
<i class="fas fa-tag mr-2"></i>
<span>{{ selectedProject ? getProjectName(selectedProject) : 'Add project' }}</span>
</button>
<div v-if="showProjectDropdown" class="absolute right-0 mt-1 w-48 bg-white rounded-md shadow-lg z-10">
<div class="py-1">
<a v-for="project in projects" :key="project.id"
href="#"
@click.prevent="selectProject(project.id); showProjectDropdown = false"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<div class="flex items-center">
<div class="w-3 h-3 rounded-full mr-2" :style="{backgroundColor: project.color}"></div>
{{ project.name }}
</div>
</a>
<div class="border-t border-gray-100"></div>
<a href="#" @click.prevent="showProjectModal = true; showProjectDropdown = false" class="block px-4 py-2 text-sm text-blue-500 hover:bg-gray-100">
<i class="fas fa-plus mr-2"></i> New Project
</a>
</div>
</div>
</div>
<button @click="showTaskModal = true" class="bg-gray-100 hover:bg-gray-200 text-gray-700 py-2 px-4 rounded">
<i class="fas fa-tasks mr-2"></i>{{ currentTask ? currentTask : 'Add task' }}
</button>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex-1">
<input v-model="currentDescription"
type="text"
placeholder="What are you working on?"
class="w-full border-0 text-2xl font-medium focus:ring-0 focus:outline-none">
</div>
<div class="flex items-center space-x-4">
<div class="text-3xl font-mono font-bold">{{ formatTime(hours) }}:{{ formatTime(minutes) }}:{{ formatTime(seconds) }}</div>
<button @click="toggleTimer"
class="rounded-full w-12 h-12 flex items-center justify-center text-white"
:class="{'bg-red-500 hover:bg-red-600': timerRunning, 'bg-green-500 hover:bg-green-600': !timerRunning}">
<i :class="{'fas fa-stop': timerRunning, 'fas fa-play': !timerRunning}"></i>
</button>
</div>
</div>
<div class="mt-4 flex items-center text-sm text-gray-500">
<span>{{ getCurrentDate() }} • {{ timerRunning ? 'Started at ' + formatStartTime() : 'Not running' }} • {{ selectedProject ? getProjectName(selectedProject) : 'No project' }}</span>
</div>
</div>
<!-- Recent Time Entries -->
<div class="px-6 pb-6">
<div class="bg-white rounded-lg shadow-sm">
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-semibold">{{ getTimeEntriesTitle() }}</h2>
<div class="flex space-x-2">
<button @click="showDateFilter = !showDateFilter" class="text-gray-500 hover:text-gray-700 text-sm flex items-center">
<i class="fas fa-calendar-alt mr-1"></i>
<span>{{ formatFilterDate() }}</span>
</button>
<div v-if="showDateFilter" class="absolute right-6 mt-8 bg-white rounded-md shadow-lg p-4 z-10">
<div class="flex items-center mb-2">
<input v-model="dateFilter" type="date" class="border rounded px-2 py-1">
<button @click="showDateFilter = false" class="ml-2 text-blue-500">Apply</button>
</div>
</div>
</div>
</div>
<div class="divide-y divide-gray-200">
<div v-for="entry in filteredTimeEntries" :key="entry.id"
class="time-entry p-4 hover:bg-gray-50 transition-colors duration-150">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div v-if="entry.projectId" class="w-3 h-3 rounded-full mr-3" :style="{backgroundColor: getProjectColor(entry.projectId)}"></div>
<div>
<p class="font-medium">{{ entry.description || 'No description' }}</p>
<p class="text-sm text-gray-500">
{{ entry.task ? entry.task + ' • ' : '' }}
{{ entry.projectId ? getProjectName(entry.projectId) : 'No project' }}
</p>
</div>
</div>
<div class="flex items-center">
<div class="font-mono mr-6">{{ formatDuration(entry.duration) }}</div>
<div class="entry-actions opacity-0 flex space-x-2">
<button @click="editTimeEntry(entry)" class="text-gray-400 hover:text-blue-500">
<i class="fas fa-edit"></i>
</button>
<button @click="deleteTimeEntry(entry.id)" class="text-gray-400 hover:text-red-500">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
<div v-if="filteredTimeEntries.length === 0" class="p-4 text-center text-gray-500">
No time entries for this date
</div>
</div>
</div>
</div>
</div>
<!-- Projects View -->
<div v-if="activeView === 'projects'" class="flex-1 overflow-y-auto px-6 py-6">
<div class="bg-white rounded-lg shadow-sm">
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-semibold">Projects</h2>
<button @click="showProjectModal = true" class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded">
<i class="fas fa-plus mr-2"></i> New Project
</button>
</div>
<div class="divide-y divide-gray-200">
<div v-for="project in projects" :key="project.id" class="p-4 hover:bg-gray-50 transition-colors duration-150">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="w-4 h-4 rounded-full mr-3" :style="{backgroundColor: project.color}"></div>
<div>
<p class="font-medium">{{ project.name }}</p>
<p class="text-sm text-gray-500">{{ getProjectTime(project.id) }} tracked</p>
</div>
</div>
<div class="flex items-center space-x-4">
<button @click="editProject(project)" class="text-gray-400 hover:text-blue-500">
<i class="fas fa-edit"></i>
</button>
<button @click="deleteProject(project.id)" class="text-gray-400 hover:text-red-500" :disabled="project.id === selectedProject">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<div v-if="projects.length === 0" class="p-4 text-center text-gray-500">
No projects yet. Create your first project to start tracking time.
</div>
</div>
</div>
</div>
<!-- Reports View -->
<div v-if="activeView === 'reports'" class="flex-1 overflow-y-auto px-6 py-6">
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold mb-6">Time Summary</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div class="bg-blue-50 p-4 rounded-lg">
<p class="text-sm text-blue-500 font-medium">Today</p>
<p class="text-2xl font-bold">{{ getTotalTime('today') }}</p>
</div>
<div class="bg-green-50 p-4 rounded-lg">
<p class="text-sm text-green-500 font-medium">This Week</p>
<p class="text-2xl font-bold">{{ getTotalTime('week') }}</p>
</div>
<div class="bg-purple-50 p-4 rounded-lg">
<p class="text-sm text-purple-500 font-medium">This Month</p>
<p class="text-2xl font-bold">{{ getTotalTime('month') }}</p>
</div>
</div>
<h3 class="text-md font-semibold mb-4">Project Breakdown</h3>
<div class="space-y-4">
<div v-for="project in projects" :key="project.id" class="mb-2">
<div class="flex justify-between mb-1">
<div class="flex items-center">
<div class="w-3 h-3 rounded-full mr-2" :style="{backgroundColor: project.color}"></div>
<span>{{ project.name }}</span>
</div>
<span class="font-medium">{{ getProjectTime(project.id) }}</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="h-2 rounded-full" :style="{width: getProjectPercentage(project.id) + '%', backgroundColor: project.color}"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Dashboard View -->
<div v-if="activeView === 'dashboard'" class="flex-1 overflow-y-auto px-6 py-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold mb-4">Recent Activity</h2>
<div class="space-y-4">
<div v-for="entry in recentTimeEntries" :key="entry.id" class="flex items-start">
<div class="w-3 h-3 rounded-full mt-1 mr-3" :style="{backgroundColor: entry.projectId ? getProjectColor(entry.projectId) : '#ccc'}"></div>
<div class="flex-1">
<p class="font-medium">{{ entry.description || 'No description' }}</p>
<p class="text-sm text-gray-500">
{{ formatDate(entry.startTime) }} • {{ formatDuration(entry.duration) }}
</p>
</div>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold mb-4">Time Distribution</h2>
<div class="h-64">
<canvas id="timeChart"></canvas>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold mb-4">Daily Summary</h2>
<div class="h-64">
<canvas id="dailyChart"></canvas>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold mb-4">Quick Actions</h2>
<div class="grid grid-cols-2 gap-4">
<button @click="activeView = 'tracker'" class="bg-blue-50 hover:bg-blue-100 text-blue-500 p-4 rounded-lg flex flex-col items-center">
<i class="fas fa-clock text-2xl mb-2"></i>
<span>Start Timer</span>
</button>
<button @click="showProjectModal = true" class="bg-green-50 hover:bg-green-100 text-green-500 p-4 rounded-lg flex flex-col items-center">
<i class="fas fa-plus text-2xl mb-2"></i>
<span>New Project</span>
</button>
<button @click="activeView = 'reports'" class="bg-purple-50 hover:bg-purple-100 text-purple-500 p-4 rounded-lg flex flex-col items-center">
<i class="fas fa-chart-bar text-2xl mb-2"></i>
<span>View Reports</span>
</button>
<button @click="exportData" class="bg-yellow-50 hover:bg-yellow-100 text-yellow-500 p-4 rounded-lg flex flex-col items-center">
<i class="fas fa-file-export text-2xl mb-2"></i>
<span>Export Data</span>
</button>
</div>
</div>
</div>
</div>
<!-- Summary Footer -->
<div class="bg-white border-t border-gray-200 py-3 px-6 flex items-center justify-between">
<div class="text-sm text-gray-500">
<span>Total today: <span class="font-mono font-medium">{{ getTotalTime('today') }}</span></span>
</div>
<div class="flex items-center space-x-4">
<button @click="exportData" class="text-blue-500 hover:text-blue-700 text-sm font-medium">
<i class="fas fa-file-export mr-2"></i>Export
</button>
<button @click="printReport" class="text-blue-500 hover:text-blue-700 text-sm font-medium">
<i class="fas fa-print mr-2"></i>Print
</button>
</div>
</div>
</div>
</div>
<!-- Project Modal -->
<div v-if="showProjectModal" class="fixed inset-0 flex items-center justify-center z-50 modal-overlay">
<div class="modal bg-white rounded-lg shadow-xl w-full max-w-md">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">{{ editingProject ? 'Edit Project' : 'New Project' }}</h3>
<button @click="closeProjectModal" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Project Name</label>
<input v-model="newProject.name" type="text" class="w-full border rounded px-3 py-2">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Color</label>
<div class="flex space-x-2">
<div v-for="color in projectColors" :key="color"
@click="newProject.color = color"
class="w-8 h-8 rounded-full cursor-pointer border-2"
:class="{'border-blue-500': newProject.color === color, 'border-transparent': newProject.color !== color}"
:style="{backgroundColor: color}"></div>
</div>
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button @click="closeProjectModal" class="px-4 py-2 border rounded text-gray-700 hover:bg-gray-50">
Cancel
</button>
<button @click="saveProject" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
{{ editingProject ? 'Update' : 'Create' }}
</button>
</div>
</div>
</div>
</div>
<!-- Task Modal -->
<div v-if="showTaskModal" class="fixed inset-0 flex items-center justify-center z-50 modal-overlay">
<div class="modal bg-white rounded-lg shadow-xl w-full max-w-md">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Add Task</h3>
<button @click="closeTaskModal" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Task Name</label>
<input v-model="newTask" type="text" class="w-full border rounded px-3 py-2" placeholder="e.g. Design, Development, Meeting">
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button @click="closeTaskModal" class="px-4 py-2 border rounded text-gray-700 hover:bg-gray-50">
Cancel
</button>
<button @click="saveTask" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Save
</button>
</div>
</div>
</div>
</div>
<!-- Time Entry Modal -->
<div v-if="showTimeEntryModal" class="fixed inset-0 flex items-center justify-center z-50 modal-overlay">
<div class="modal bg-white rounded-lg shadow-xl w-full max-w-md">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Edit Time Entry</h3>
<button @click="closeTimeEntryModal" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<input v-model="editingEntry.description" type="text" class="w-full border rounded px-3 py-2">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Project</label>
<select v-model="editingEntry.projectId" class="w-full border rounded px-3 py-2">
<option value="">No project</option>
<option v-for="project in projects" :key="project.id" :value="project.id">
{{ project.name }}
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Task</label>
<input v-model="editingEntry.task" type="text" class="w-full border rounded px-3 py-2">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Start Time</label>
<input v-model="editingEntry.startTime" type="datetime-local" class="w-full border rounded px-3 py-2">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Duration</label>
<input v-model="editingEntry.duration" type="text" class="w-full border rounded px-3 py-2" placeholder="HH:MM:SS">
</div>
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button @click="closeTimeEntryModal" class="px-4 py-2 border rounded text-gray-700 hover:bg-gray-50">
Cancel
</button>
<button @click="saveTimeEntry" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Update
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.47/dist/vue.global.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const { createApp, ref, computed, onMounted, watch, nextTick } = Vue;
createApp({
setup() {
// State
const activeView = ref('tracker');
const sidebarCollapsed = ref(false);
const timerRunning = ref(false);
const hours = ref(0);
const minutes = ref(0);
const seconds = ref(0);
const timerInterval = ref(null);
const startTime = ref(null);
const currentDescription = ref('');
const currentTask = ref('');
const selectedProject = ref(null);
const showProjectDropdown = ref(false);
const showDateFilter = ref(false);
const dateFilter = ref(new Date().toISOString().split('T')[0]);
const timeEntries = ref([]);
const projects = ref([]);
const showProjectModal = ref(false);
const showTaskModal = ref(false);
const showTimeEntryModal = ref(false);
const editingProject = ref(null);
const editingEntry = ref(null);
const newProject = ref({
name: '',
color: '#3b82f6' // blue-500
});
const newTask = ref('');
const timeChart = ref(null);
const dailyChart = ref(null);
// Project colors
const projectColors = ref([
'#3b82f6', // blue-500
'#ef4444', // red-500
'#10b981', // green-500
'#f59e0b', // yellow-500
'#8b5cf6', // purple-500
'#ec4899', // pink-500
'#14b8a6', // teal-500
'#f97316' // orange-500
]);
// Load data from localStorage
const loadData = () => {
const savedTimeEntries = localStorage.getItem('timeEntries');
const savedProjects = localStorage.getItem('projects');
const savedSelectedProject = localStorage.getItem('selectedProject');
if (savedTimeEntries) timeEntries.value = JSON.parse(savedTimeEntries);
if (savedProjects) projects.value = JSON.parse(savedProjects);
if (savedSelectedProject) selectedProject.value = savedSelectedProject;
};
// Save data to localStorage
const saveData = () => {
localStorage.setItem('timeEntries', JSON.stringify(timeEntries.value));
localStorage.setItem('projects', JSON.stringify(projects.value));
localStorage.setItem('selectedProject', selectedProject.value);
};
// Initialize
onMounted(() => {
loadData();
// Start with some sample data if empty
if (projects.value.length === 0) {
projects.value = [
{ id: '1', name: 'Website Redesign', color: '#ef4444' },
{ id: '2', name: 'Marketing Campaign', color: '#10b981' },
{ id: '3', name: 'Mobile App Development', color: '#f59e0b' }
];
saveData();
}
if (timeEntries.value.length === 0) {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
timeEntries.value = [
{
id: '1',
description: 'Marketing strategy meeting',
projectId: '2',
task: 'Meeting',
startTime: new Date(today.getTime() + 9 * 60 * 60 * 1000).toISOString(),
duration: 2 * 60 * 60 + 14 * 60 + 36 // 2:14:36 in seconds
},
{
id: '2',
description: 'App UI redesign',
projectId: '3',
task: 'Design',
startTime: new Date(today.getTime() + 11 * 60 * 60 * 1000 + 30 * 60 * 1000).toISOString(),
duration: 1 * 60 * 60 + 45 * 60 + 22 // 1:45:22 in seconds
},
{
id: '3',
description: 'Client support call',
projectId: '1',
task: 'Support',
startTime: new Date(today.getTime() + 14 * 60 * 60 * 1000).toISOString(),
duration: 38 * 60 + 14 // 0:38:14 in seconds
}
];
saveData();
}
// Initialize charts when reports view is loaded
watch(activeView, (newVal) => {
if (newVal === 'reports' || newVal === 'dashboard') {
nextTick(() => {
initCharts();
});
}
}, { immediate: true });
});
// Initialize charts
const initCharts = () => {
// Time distribution chart (pie)
const timeCtx = document.getElementById('timeChart');
if (timeCtx) {
if (timeChart.value) timeChart.value.destroy();
const projectTimes = projects.value.map(project => {
return timeEntries.value
.filter(entry => entry.projectId === project.id)
.reduce((sum, entry) => sum + entry.duration, 0);
});
timeChart.value = new Chart(timeCtx, {
type: 'pie',
data: {
labels: projects.value.map(p => p.name),
datasets: [{
data: projectTimes,
backgroundColor: projects.value.map(p => p.color),
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right'
}
}
}
});
}
// Daily summary chart (bar)
const dailyCtx = document.getElementById('dailyChart');
if (dailyCtx) {
if (dailyChart.value) dailyChart.value.destroy();
// Get last 7 days
const days = [];
const dayData = [];
const now = new Date();
for (let i = 6; i >= 0; i--) {
const day = new Date(now);
day.setDate(now.getDate() - i);
days.push(day.toLocaleDateString('en-US', { weekday: 'short' }));
const dayStart = new Date(day);
dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(day);
dayEnd.setHours(23, 59, 59, 999);
const dayEntries = timeEntries.value.filter(entry => {
const entryDate = new Date(entry.startTime);
return entryDate >= dayStart && entryDate <= dayEnd;
});
const totalSeconds = dayEntries.reduce((sum, entry) => sum + entry.duration, 0);
dayData.push(totalSeconds / 3600); // Convert to hours
}
dailyChart.value = new Chart(dailyCtx, {
type: 'bar',
data: {
labels: days,
datasets: [{
label: 'Hours',
data: dayData,
backgroundColor: '#3b82f6',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Hours'
}
}
}
}
});
}
};
// Computed properties
const filteredTimeEntries = computed(() => {
const filterDate = new Date(dateFilter.value);
const startOfDay = new Date(filterDate.getFullYear(), filterDate.getMonth(), filterDate.getDate());
const endOfDay = new Date(filterDate.getFullYear(), filterDate.getMonth(), filterDate.getDate() + 1);
return timeEntries.value
.filter(entry => {
const entryDate = new Date(entry.startTime);
return entryDate >= startOfDay && entryDate < endOfDay;
})
.sort((a, b) => new Date(b.startTime) - new Date(a.startTime));
});
const recentTimeEntries = computed(() => {
return [...timeEntries.value]
.sort((a, b) => new Date(b.startTime) - new Date(a.startTime))
.slice(0, 5);
});
// Methods
const toggleSidebar = () => {
sidebarCollapsed.value = !sidebarCollapsed.value;
};
const startTimer = () => {
timerRunning.value = true;
startTime.value = new Date();
hours.value = 0;
minutes.value = 0;
seconds.value = 0;
timerInterval.value = setInterval(() => {
seconds.value++;
if (seconds.value >= 60) {
seconds.value = 0;
minutes.value++;
if (minutes.value >= 60) {
minutes.value = 0;
hours.value++;
}
}
}, 1000);
};
const stopTimer = () => {
timerRunning.value = false;
clearInterval(timerInterval.value);
if (hours.value > 0 || minutes.value > 0 || seconds.value > 0) {
const duration = hours.value * 3600 + minutes.value * 60 + seconds.value;
timeEntries.value.push({
id: Date.now().toString(),
description: currentDescription.value,
projectId: selectedProject.value,
task: currentTask.value,
startTime: startTime.value.toISOString(),
duration: duration
});
saveData();
// Reset timer but keep description and project
hours.value = 0;
minutes.value = 0;
seconds.value = 0;
}
};
const toggleTimer = () => {
if (timerRunning.value) {
stopTimer();
} else {
startTimer();
}
};
const formatTime = (value) => {
return value < 10 ? `0${value}` : value;
};
const formatDuration = (seconds) => {
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${formatTime(hrs)}:${formatTime(mins)}:${formatTime(secs)}`;
};
const formatStartTime = () => {
if (!startTime.value) return '';
return startTime.value.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
const getCurrentDate = () => {
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
return new Date().toLocaleDateString(undefined, options);
};
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
};
const formatFilterDate = () => {
const date = new Date(dateFilter.value);
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
};
const getActiveViewTitle = () => {
const titles = {
'dashboard': 'Dashboard',
'tracker': 'Time Tracker',
'reports': 'Reports',
'projects': 'Projects'
};
return titles[activeView.value] || 'TimeTrack Pro';
};
const getTimeEntriesTitle = () => {
const today = new Date().toISOString().split('T')[0];
if (dateFilter.value === today) {
return "Today's time entries";
} else {
return `Time entries for ${formatFilterDate()}`;
}
};
const selectProject = (projectId) => {
selectedProject.value = projectId;
saveData();
};
const getProjectName = (projectId) => {
const project = projects.value.find(p => p.id === projectId);
return project ? project.name : 'Unknown';
};
const getProjectColor = (projectId) => {
const project = projects.value.find(p => p.id === projectId);
return project ? project.color : '#ccc';
};
const getProjectTime = (projectId) => {
const totalSeconds = timeEntries.value
.filter(entry => entry.projectId === projectId)
.reduce((sum, entry) => sum + entry.duration, 0);
return formatDuration(totalSeconds);
};
const getProjectPercentage = (projectId) => {
const projectSeconds = timeEntries.value
.filter(entry => entry.projectId === projectId)
.reduce((sum, entry) => sum + entry.duration, 0);
const totalSeconds = timeEntries.value
.reduce((sum, entry) => sum + entry.duration, 0);
if (totalSeconds === 0) return 0;
return Math.round((projectSeconds / totalSeconds) * 100);
};
const getTotalTime = (period) => {
let startDate, endDate;
const now = new Date();
if (period === 'today') {
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
} else if (period === 'week') {
const day = now.getDay();
const diff = now.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is Sunday
startDate = new Date(now.setDate(diff));
startDate.setHours(0, 0, 0, 0);
endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 7);
} else if (period === 'month') {
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
endDate = new Date(now.getFullYear(), now.getMonth() + 1, 1);
}
const filteredEntries = timeEntries.value.filter(entry => {
const entryDate = new Date(entry.startTime);
return entryDate >= startDate && entryDate < endDate;
});
const totalSeconds = filteredEntries.reduce((sum, entry) => sum + entry.duration, 0);
return formatDuration(totalSeconds);
};
const openProjectModal = () => {
showProjectModal.value = true;
editingProject.value = null;
newProject.value = { name: '', color: '#3b82f6' };
};
const closeProjectModal = () => {
showProjectModal.value = false;
};
const saveProject = () => {
if (!newProject.value.name.trim()) return;
if (editingProject.value) {
// Update existing project
const index = projects.value.findIndex(p => p.id === editingProject.value.id);
if (index !== -1) {
projects.value[index] = { ...editingProject.value, ...newProject.value };
}
} else {
// Add new project
projects.value.push({
id: Date.now().toString(),
...newProject.value
});
}
saveData();
closeProjectModal();
};
const editProject = (project) => {
editingProject.value = { ...project };
newProject.value = { name: project.name, color: project.color };
showProjectModal.value = true;
};
const deleteProject = (projectId) => {
if (confirm('Are you sure you want to delete this project? This will not delete time entries associated with it.')) {
projects.value = projects.value.filter(p => p.id !== projectId);
// Clear selected project if it's the one being deleted
if (selectedProject.value === projectId) {
selectedProject.value = null;
}
saveData();
}
};
const closeTaskModal = () => {
showTaskModal.value = false;
newTask.value = '';
};
const saveTask = () => {
if (newTask.value.trim()) {
currentTask.value = newTask.value;
closeTaskModal();
}
};
const editTimeEntry = (entry) => {
editingEntry.value = { ...entry };
showTimeEntryModal.value = true;
};
const closeTimeEntryModal = () => {
showTimeEntryModal.value = false;
editingEntry.value = null;
};
const saveTimeEntry = () => {
if (!editingEntry.value) return;
// Parse duration from HH:MM:SS to seconds
const durationParts = editingEntry.value.duration.split(':').map(part => parseInt(part));
let durationSeconds = 0;
if (durationParts.length === 3) {
durationSeconds = durationParts[0] * 3600 + durationParts[1] * 60 + durationParts[2];
} else if (durationParts.length === 2) {
durationSeconds = durationParts[0] * 60 + durationParts[1];
} else if (durationParts.length === 1) {
durationSeconds = durationParts[0];
}
editingEntry.value.duration = durationSeconds;
// Update the entry
const index = timeEntries.value.findIndex(e => e.id === editingEntry.value.id);
if (index !== -1) {
timeEntries.value[index] = { ...editingEntry.value };
}
saveData();
closeTimeEntryModal();
};
const deleteTimeEntry = (id) => {
if (confirm('Are you sure you want to delete this time entry?')) {
timeEntries.value = timeEntries.value.filter(entry => entry.id !== id);
saveData();
}
};
const exportData = () => {
const data = {
projects: projects.value,
timeEntries: timeEntries.value
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `timetrack-export-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const printReport = () => {
window.print();
};
// Watch for changes to update charts
watch([timeEntries, projects], () => {
if (activeView.value === 'reports' || activeView.value === 'dashboard') {
initCharts();
}
}, { deep: true });
return {
// State
activeView,
sidebarCollapsed,
timerRunning,
hours,
minutes,
seconds,
currentDescription,
currentTask,
selectedProject,
showProjectDropdown,
showDateFilter,
dateFilter,
showProjectModal,
showTaskModal,
showTimeEntryModal,
editingProject,
editingEntry,
newProject,
newTask,
projectColors,
// Computed
filteredTimeEntries,
recentTimeEntries,
// Methods
toggleSidebar,
toggleTimer,
formatTime,
formatDuration,
formatStartTime,
getCurrentDate,
formatDate,
formatFilterDate,
getActiveViewTitle,
getTimeEntriesTitle,
selectProject,
getProjectName,
getProjectColor,
getProjectTime,
getProjectPercentage,
getTotalTime,
openProjectModal,
closeProjectModal,
saveProject,
editProject,
deleteProject,
closeTaskModal,
saveTask,
editTimeEntry,
closeTimeEntryModal,
saveTimeEntry,
deleteTimeEntry,
exportData,
printReport
};
}
}).mount('#app');
</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=Khulekanis/testing-space" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>