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:
2025-07-07 21:16:36 +02:00
parent 4214e88d18
commit 9a79778ad6
116 changed files with 21063 additions and 5653 deletions

View File

@@ -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
View 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);