Merge branch 'master' into feature-markdown-notes

This commit is contained in:
2025-07-07 21:26:44 +02:00
122 changed files with 23364 additions and 8651 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);