Squashed commit of the following:
commit 1eeea9f83ad9230a5c1f7a75662770eaab0df837 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 21:15:41 2025 +0200 Disable resuming of old time entries. commit 3e3ec2f01cb7943622b819a19179388078ae1315 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 20:59:19 2025 +0200 Refactor db migrations. commit 15a51a569da36c6b7c9e01ab17b6fdbdee6ad994 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 19:58:04 2025 +0200 Apply new style for Time Tracking view. commit 77e5278b303e060d2b03853b06277f8aa567ae68 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 18:06:04 2025 +0200 Allow direct registrations as a Company. commit 188a8772757cbef374243d3a5f29e4440ddecabe Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 18:04:45 2025 +0200 Add email invitation feature. commit d9ebaa02aa01b518960a20dccdd5a327d82f30c6 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 17:12:32 2025 +0200 Apply common style for Company, User, Team management pages. commit 81149caf4d8fc6317e2ab1b4f022b32fc5aa6d22 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 16:44:32 2025 +0200 Move export functions to own module. commit 1a26e19338e73f8849c671471dd15cc3c1b1fe82 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 15:51:15 2025 +0200 Split up models.py. commit 61f1ccd10f721b0ff4dc1eccf30c7a1ee13f204d Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 12:05:28 2025 +0200 Move utility function into own modules. commit 84b341ed35e2c5387819a8b9f9d41eca900ae79f Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 11:44:24 2025 +0200 Refactor auth functions use. commit 923e311e3da5b26d85845c2832b73b7b17c48adb Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 11:35:52 2025 +0200 Refactor route nameing and fix bugs along the way. commit f0a5c4419c340e62a2615c60b2a9de28204d2995 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 10:34:33 2025 +0200 Fix URL endpoints in announcement template. commit b74d74542a1c8dc350749e4788a9464d067a88b5 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 09:25:53 2025 +0200 Move announcements to own module. commit 9563a28021ac46c82c04fe4649b394dbf96f92c7 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 09:16:30 2025 +0200 Combine Company view and edit templates. commit 6687c373e681d54e4deab6b2582fed5cea9aadf6 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 08:17:42 2025 +0200 Move Users, Company and System Administration to own modules. commit 8b7894a2e3eb84bb059f546648b6b9536fea724e Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 07:40:57 2025 +0200 Move Teams and Projects to own modules. commit d11bf059d99839ecf1f5d7020b8c8c8a2454c00b Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 07:09:33 2025 +0200 Move Tasks and Sprints to own modules.
This commit is contained in:
@@ -73,7 +73,7 @@ function renderSubtasks() {
|
||||
function addSubtask() {
|
||||
const newSubtask = {
|
||||
name: '',
|
||||
status: 'NOT_STARTED',
|
||||
status: 'TODO',
|
||||
priority: 'MEDIUM',
|
||||
assigned_to_id: null,
|
||||
isNew: true
|
||||
@@ -143,7 +143,7 @@ function updateSubtaskAssignee(index, assigneeId) {
|
||||
// Toggle subtask status
|
||||
function toggleSubtaskStatus(index) {
|
||||
const subtask = currentSubtasks[index];
|
||||
const newStatus = subtask.status === 'COMPLETED' ? 'NOT_STARTED' : 'COMPLETED';
|
||||
const newStatus = subtask.status === 'DONE' ? 'TODO' : 'DONE';
|
||||
|
||||
if (subtask.id) {
|
||||
// Update in database
|
||||
|
||||
445
static/js/time-tracking.js
Normal file
445
static/js/time-tracking.js
Normal file
@@ -0,0 +1,445 @@
|
||||
// Time Tracking JavaScript
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Project/Task Selection
|
||||
const projectSelect = document.getElementById('project-select');
|
||||
const taskSelect = document.getElementById('task-select');
|
||||
const manualProjectSelect = document.getElementById('manual-project');
|
||||
const manualTaskSelect = document.getElementById('manual-task');
|
||||
|
||||
// Update task dropdown when project is selected
|
||||
function updateTaskDropdown(projectSelectElement, taskSelectElement) {
|
||||
const projectId = projectSelectElement.value;
|
||||
|
||||
if (!projectId) {
|
||||
taskSelectElement.disabled = true;
|
||||
taskSelectElement.innerHTML = '<option value="">Select a project first</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch tasks for the selected project
|
||||
fetch(`/api/projects/${projectId}/tasks`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(data => {
|
||||
throw new Error(data.error || `HTTP error! status: ${response.status}`);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
taskSelectElement.disabled = false;
|
||||
taskSelectElement.innerHTML = '<option value="">No specific task</option>';
|
||||
|
||||
if (data.tasks && data.tasks.length > 0) {
|
||||
data.tasks.forEach(task => {
|
||||
const option = document.createElement('option');
|
||||
option.value = task.id;
|
||||
option.textContent = `${task.title} (${task.status})`;
|
||||
taskSelectElement.appendChild(option);
|
||||
});
|
||||
} else {
|
||||
taskSelectElement.innerHTML = '<option value="">No tasks available</option>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching tasks:', error);
|
||||
taskSelectElement.disabled = true;
|
||||
taskSelectElement.innerHTML = `<option value="">Error: ${error.message}</option>`;
|
||||
});
|
||||
}
|
||||
|
||||
if (projectSelect) {
|
||||
projectSelect.addEventListener('change', () => updateTaskDropdown(projectSelect, taskSelect));
|
||||
}
|
||||
|
||||
if (manualProjectSelect) {
|
||||
manualProjectSelect.addEventListener('change', () => updateTaskDropdown(manualProjectSelect, manualTaskSelect));
|
||||
}
|
||||
|
||||
// Start Work Form
|
||||
const startWorkForm = document.getElementById('start-work-form');
|
||||
if (startWorkForm) {
|
||||
startWorkForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const projectId = document.getElementById('project-select').value;
|
||||
const taskId = document.getElementById('task-select').value;
|
||||
const notes = document.getElementById('work-notes').value;
|
||||
|
||||
fetch('/api/arrive', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
project_id: projectId || null,
|
||||
task_id: taskId || null,
|
||||
notes: notes || null
|
||||
}),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
showNotification('Error: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotification('An error occurred while starting work', 'error');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// View Toggle
|
||||
const viewToggleBtns = document.querySelectorAll('.toggle-btn');
|
||||
const listView = document.getElementById('list-view');
|
||||
const gridView = document.getElementById('grid-view');
|
||||
|
||||
viewToggleBtns.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const view = this.getAttribute('data-view');
|
||||
|
||||
// Update button states
|
||||
viewToggleBtns.forEach(b => b.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// Show/hide views
|
||||
if (view === 'list') {
|
||||
listView.classList.add('active');
|
||||
gridView.classList.remove('active');
|
||||
} else {
|
||||
listView.classList.remove('active');
|
||||
gridView.classList.add('active');
|
||||
}
|
||||
|
||||
// Save preference
|
||||
localStorage.setItem('timeTrackingView', view);
|
||||
});
|
||||
});
|
||||
|
||||
// Restore view preference
|
||||
const savedView = localStorage.getItem('timeTrackingView') || 'list';
|
||||
if (savedView === 'grid') {
|
||||
document.querySelector('[data-view="grid"]').click();
|
||||
}
|
||||
|
||||
// Modal Functions
|
||||
function openModal(modalId) {
|
||||
document.getElementById(modalId).style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
document.getElementById(modalId).style.display = 'none';
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
// Modal close buttons
|
||||
document.querySelectorAll('.modal-close, .modal-cancel').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const modal = this.closest('.modal');
|
||||
closeModal(modal.id);
|
||||
});
|
||||
});
|
||||
|
||||
// Close modal on overlay click
|
||||
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||||
overlay.addEventListener('click', function() {
|
||||
const modal = this.closest('.modal');
|
||||
closeModal(modal.id);
|
||||
});
|
||||
});
|
||||
|
||||
// Manual Entry
|
||||
const manualEntryBtn = document.getElementById('manual-entry-btn');
|
||||
if (manualEntryBtn) {
|
||||
manualEntryBtn.addEventListener('click', function() {
|
||||
// Set default dates to today
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('manual-start-date').value = today;
|
||||
document.getElementById('manual-end-date').value = today;
|
||||
openModal('manual-modal');
|
||||
});
|
||||
}
|
||||
|
||||
// Manual Entry Form Submission
|
||||
const manualEntryForm = document.getElementById('manual-entry-form');
|
||||
if (manualEntryForm) {
|
||||
manualEntryForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const startDate = document.getElementById('manual-start-date').value;
|
||||
const startTime = document.getElementById('manual-start-time').value;
|
||||
const endDate = document.getElementById('manual-end-date').value;
|
||||
const endTime = document.getElementById('manual-end-time').value;
|
||||
const projectId = document.getElementById('manual-project').value;
|
||||
const taskId = document.getElementById('manual-task').value;
|
||||
const breakMinutes = parseInt(document.getElementById('manual-break').value) || 0;
|
||||
const notes = document.getElementById('manual-notes').value;
|
||||
|
||||
// Validate end time is after start time
|
||||
const startDateTime = new Date(`${startDate}T${startTime}`);
|
||||
const endDateTime = new Date(`${endDate}T${endTime}`);
|
||||
|
||||
if (endDateTime <= startDateTime) {
|
||||
showNotification('End time must be after start time', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/manual-entry', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
start_date: startDate,
|
||||
start_time: startTime,
|
||||
end_date: endDate,
|
||||
end_time: endTime,
|
||||
project_id: projectId || null,
|
||||
task_id: taskId || null,
|
||||
break_minutes: breakMinutes,
|
||||
notes: notes || null
|
||||
}),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
closeModal('manual-modal');
|
||||
location.reload();
|
||||
} else {
|
||||
showNotification('Error: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotification('An error occurred while adding the manual entry', 'error');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Edit Entry
|
||||
document.querySelectorAll('.edit-entry-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const entryId = this.getAttribute('data-id');
|
||||
|
||||
// Fetch entry details
|
||||
fetch(`/api/time-entry/${entryId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const entry = data.entry;
|
||||
|
||||
// Parse dates and times
|
||||
const arrivalDate = new Date(entry.arrival_time);
|
||||
const departureDate = entry.departure_time ? new Date(entry.departure_time) : null;
|
||||
|
||||
// Set form values
|
||||
document.getElementById('edit-entry-id').value = entry.id;
|
||||
document.getElementById('edit-arrival-date').value = arrivalDate.toISOString().split('T')[0];
|
||||
document.getElementById('edit-arrival-time').value = arrivalDate.toTimeString().substring(0, 5);
|
||||
|
||||
if (departureDate) {
|
||||
document.getElementById('edit-departure-date').value = departureDate.toISOString().split('T')[0];
|
||||
document.getElementById('edit-departure-time').value = departureDate.toTimeString().substring(0, 5);
|
||||
} else {
|
||||
document.getElementById('edit-departure-date').value = '';
|
||||
document.getElementById('edit-departure-time').value = '';
|
||||
}
|
||||
|
||||
document.getElementById('edit-project').value = entry.project_id || '';
|
||||
document.getElementById('edit-notes').value = entry.notes || '';
|
||||
|
||||
openModal('edit-modal');
|
||||
} else {
|
||||
showNotification('Error loading entry details', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotification('An error occurred while loading entry details', 'error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Edit Entry Form Submission
|
||||
const editEntryForm = document.getElementById('edit-entry-form');
|
||||
if (editEntryForm) {
|
||||
editEntryForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const entryId = document.getElementById('edit-entry-id').value;
|
||||
const arrivalDate = document.getElementById('edit-arrival-date').value;
|
||||
const arrivalTime = document.getElementById('edit-arrival-time').value;
|
||||
const departureDate = document.getElementById('edit-departure-date').value;
|
||||
const departureTime = document.getElementById('edit-departure-time').value;
|
||||
const projectId = document.getElementById('edit-project').value;
|
||||
const notes = document.getElementById('edit-notes').value;
|
||||
|
||||
// Format datetime strings
|
||||
const arrivalDateTime = `${arrivalDate}T${arrivalTime}:00`;
|
||||
let departureDateTime = null;
|
||||
|
||||
if (departureDate && departureTime) {
|
||||
departureDateTime = `${departureDate}T${departureTime}:00`;
|
||||
}
|
||||
|
||||
fetch(`/api/update/${entryId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
arrival_time: arrivalDateTime,
|
||||
departure_time: departureDateTime,
|
||||
project_id: projectId || null,
|
||||
notes: notes || null
|
||||
}),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
closeModal('edit-modal');
|
||||
location.reload();
|
||||
} else {
|
||||
showNotification('Error: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotification('An error occurred while updating the entry', 'error');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Delete Entry
|
||||
document.querySelectorAll('.delete-entry-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const entryId = this.getAttribute('data-id');
|
||||
document.getElementById('delete-entry-id').value = entryId;
|
||||
openModal('delete-modal');
|
||||
});
|
||||
});
|
||||
|
||||
// Confirm Delete
|
||||
const confirmDeleteBtn = document.getElementById('confirm-delete');
|
||||
if (confirmDeleteBtn) {
|
||||
confirmDeleteBtn.addEventListener('click', function() {
|
||||
const entryId = document.getElementById('delete-entry-id').value;
|
||||
|
||||
fetch(`/api/delete/${entryId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
closeModal('delete-modal');
|
||||
// Remove the row/card from the DOM
|
||||
const row = document.querySelector(`tr[data-entry-id="${entryId}"]`);
|
||||
const card = document.querySelector(`.entry-card[data-entry-id="${entryId}"]`);
|
||||
if (row) row.remove();
|
||||
if (card) card.remove();
|
||||
showNotification('Entry deleted successfully', 'success');
|
||||
} else {
|
||||
showNotification('Error: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotification('An error occurred while deleting the entry', 'error');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Resume Work
|
||||
document.querySelectorAll('.resume-work-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
// Skip if button is disabled
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entryId = this.getAttribute('data-id');
|
||||
|
||||
fetch(`/api/resume/${entryId}`, {
|
||||
method: 'POST',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
showNotification('Error: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotification('An error occurred while resuming work', 'error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Notification function
|
||||
function showNotification(message, type = 'info') {
|
||||
// Create notification element
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.textContent = message;
|
||||
|
||||
// Add to page
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => notification.classList.add('show'), 10);
|
||||
|
||||
// Remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('show');
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
// Add notification styles
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transform: translateX(400px);
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 9999;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.notification.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.notification-success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
border: 1px solid #6ee7b7;
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fca5a5;
|
||||
}
|
||||
|
||||
.notification-info {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
border: 1px solid #93c5fd;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
Reference in New Issue
Block a user