diff --git a/app.py b/app.py index b8300a7..a6f0864 100644 --- a/app.py +++ b/app.py @@ -3725,10 +3725,18 @@ def unified_task_management(): User.role.in_([Role.ADMIN, Role.SUPERVISOR]) ).order_by(User.username).all() + # Convert team members to JSON-serializable format + team_members_data = [{ + 'id': member.id, + 'username': member.username, + 'email': member.email, + 'role': member.role.value if member.role else 'Team Member' + } for member in team_members] + return render_template('unified_task_management.html', title='Task Management', available_projects=available_projects, - team_members=team_members) + team_members=team_members_data) # Sprint Management Route @app.route('/sprints') @@ -3815,7 +3823,14 @@ def create_task(): db.session.add(task) db.session.commit() - return jsonify({'success': True, 'message': 'Task created successfully'}) + return jsonify({ + 'success': True, + 'message': 'Task created successfully', + 'task': { + 'id': task.id, + 'task_number': task.task_number + } + }) except Exception as e: db.session.rollback() @@ -3836,17 +3851,33 @@ def get_task(task_id): task_data = { 'id': task.id, + 'task_number': getattr(task, 'task_number', f'TSK-{task.id:03d}'), 'name': task.name, 'description': task.description, 'status': task.status.name, 'priority': task.priority.name, 'estimated_hours': task.estimated_hours, 'assigned_to_id': task.assigned_to_id, + 'assigned_to_name': task.assigned_to.username if task.assigned_to else None, + 'project_id': task.project_id, + 'project_name': task.project.name if task.project else None, + 'project_code': task.project.code if task.project else None, 'start_date': task.start_date.isoformat() if task.start_date else None, - 'due_date': task.due_date.isoformat() if task.due_date else None + 'due_date': task.due_date.isoformat() if task.due_date else None, + 'completed_date': task.completed_date.isoformat() if task.completed_date else None, + 'archived_date': task.archived_date.isoformat() if task.archived_date else None, + 'sprint_id': task.sprint_id, + 'subtasks': [{ + 'id': subtask.id, + 'name': subtask.name, + 'status': subtask.status.name, + 'priority': subtask.priority.name, + 'assigned_to_id': subtask.assigned_to_id, + 'assigned_to_name': subtask.assigned_to.username if subtask.assigned_to else None + } for subtask in task.subtasks] if task.subtasks else [] } - return jsonify({'success': True, 'task': task_data}) + return jsonify(task_data) except Exception as e: return jsonify({'success': False, 'message': str(e)}) @@ -3977,6 +4008,14 @@ def get_unified_tasks(): 'created_at': task.created_at.isoformat(), 'is_team_task': is_team_task, 'subtask_count': len(task.subtasks) if task.subtasks else 0, + 'subtasks': [{ + 'id': subtask.id, + 'name': subtask.name, + 'status': subtask.status.name, + 'priority': subtask.priority.name, + 'assigned_to_id': subtask.assigned_to_id, + 'assigned_to_name': subtask.assigned_to.username if subtask.assigned_to else None + } for subtask in task.subtasks] if task.subtasks else [], 'sprint_id': task.sprint_id, 'sprint_name': task.sprint.name if task.sprint else None, 'is_current_sprint': task.sprint.is_current if task.sprint else False diff --git a/migrate_db.py b/migrate_db.py index 1b53d4c..276a842 100644 --- a/migrate_db.py +++ b/migrate_db.py @@ -684,11 +684,21 @@ def migrate_task_system(db_path): created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_by_id INTEGER NOT NULL, - FOREIGN KEY (task_id) REFERENCES task (id), + FOREIGN KEY (task_id) REFERENCES task (id) ON DELETE CASCADE, FOREIGN KEY (assigned_to_id) REFERENCES user (id), FOREIGN KEY (created_by_id) REFERENCES user (id) ) """) + + # Create index for better performance + print("Creating index on sub_task.task_id...") + cursor.execute("CREATE INDEX idx_subtask_task_id ON sub_task(task_id)") + else: + # Check if the index exists + cursor.execute("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_subtask_task_id'") + if not cursor.fetchone(): + print("Creating missing index on sub_task.task_id...") + cursor.execute("CREATE INDEX idx_subtask_task_id ON sub_task(task_id)") # Check if task_dependency table exists cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='task_dependency'") @@ -1073,6 +1083,41 @@ def migrate_postgresql_schema(): """)) db.session.commit() + # Check if sub_task table exists + result = db.session.execute(text(""" + SELECT table_name + FROM information_schema.tables + WHERE table_name = 'sub_task' + """)) + + if not result.fetchone(): + print("Creating sub_task table...") + db.session.execute(text(""" + CREATE TABLE sub_task ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) NOT NULL, + description TEXT, + status taskstatus DEFAULT 'NOT_STARTED', + priority taskpriority DEFAULT 'MEDIUM', + estimated_hours FLOAT, + task_id INTEGER NOT NULL, + assigned_to_id INTEGER, + start_date DATE, + due_date DATE, + completed_date DATE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by_id INTEGER NOT NULL, + FOREIGN KEY (task_id) REFERENCES task (id) ON DELETE CASCADE, + FOREIGN KEY (assigned_to_id) REFERENCES "user" (id), + FOREIGN KEY (created_by_id) REFERENCES "user" (id) + ) + """)) + + # Create index for better performance + db.session.execute(text("CREATE INDEX idx_subtask_task_id ON sub_task(task_id)")) + db.session.commit() + print("PostgreSQL schema migration completed successfully!") except Exception as e: diff --git a/static/js/subtasks.js b/static/js/subtasks.js new file mode 100644 index 0000000..b41ff69 --- /dev/null +++ b/static/js/subtasks.js @@ -0,0 +1,363 @@ +// Sub-task Management Functions + +// Global variable to track subtasks +let currentSubtasks = []; + +// Initialize subtasks when loading a task +function initializeSubtasks(taskId) { + currentSubtasks = []; + const subtasksContainer = document.getElementById('subtasks-container'); + if (!subtasksContainer) return; + + subtasksContainer.innerHTML = '
Loading subtasks...
'; + + if (taskId) { + // Fetch existing subtasks + fetch(`/api/tasks/${taskId}`) + .then(response => response.json()) + .then(task => { + if (task.subtasks) { + currentSubtasks = task.subtasks; + renderSubtasks(); + } else { + subtasksContainer.innerHTML = '

No subtasks yet

'; + } + }) + .catch(error => { + console.error('Error loading subtasks:', error); + subtasksContainer.innerHTML = '

Error loading subtasks

'; + }); + } else { + renderSubtasks(); + } +} + +// Render subtasks in the modal +function renderSubtasks() { + const container = document.getElementById('subtasks-container'); + if (!container) return; + + if (currentSubtasks.length === 0) { + container.innerHTML = '

No subtasks yet

'; + return; + } + + container.innerHTML = currentSubtasks.map((subtask, index) => ` +
+ + + + + +
+ `).join(''); +} + +// Add a new subtask +function addSubtask() { + const newSubtask = { + name: '', + status: 'NOT_STARTED', + priority: 'MEDIUM', + assigned_to_id: null, + isNew: true + }; + + currentSubtasks.push(newSubtask); + renderSubtasks(); + + // Focus on the new subtask input + setTimeout(() => { + const inputs = document.querySelectorAll('.subtask-name'); + if (inputs.length > 0) { + inputs[inputs.length - 1].focus(); + } + }, 50); +} + +// Remove a subtask +function removeSubtask(index) { + const subtask = currentSubtasks[index]; + + if (subtask.id) { + // If it has an ID, it exists in the database + if (confirm('Are you sure you want to delete this subtask?')) { + fetch(`/api/subtasks/${subtask.id}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => { + if (response.ok) { + currentSubtasks.splice(index, 1); + renderSubtasks(); + showNotification('Subtask deleted successfully', 'success'); + } else { + throw new Error('Failed to delete subtask'); + } + }) + .catch(error => { + console.error('Error deleting subtask:', error); + showNotification('Error deleting subtask', 'error'); + }); + } + } else { + // Just remove from array if not saved yet + currentSubtasks.splice(index, 1); + renderSubtasks(); + } +} + +// Update subtask name +function updateSubtaskName(index, name) { + currentSubtasks[index].name = name; +} + +// Update subtask priority +function updateSubtaskPriority(index, priority) { + currentSubtasks[index].priority = priority; +} + +// Update subtask assignee +function updateSubtaskAssignee(index, assigneeId) { + currentSubtasks[index].assigned_to_id = assigneeId || null; +} + +// Toggle subtask status +function toggleSubtaskStatus(index) { + const subtask = currentSubtasks[index]; + const newStatus = subtask.status === 'COMPLETED' ? 'NOT_STARTED' : 'COMPLETED'; + + if (subtask.id) { + // Update in database + fetch(`/api/subtasks/${subtask.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ status: newStatus }) + }) + .then(response => response.json()) + .then(updatedSubtask => { + currentSubtasks[index] = updatedSubtask; + renderSubtasks(); + updateTaskProgress(); + }) + .catch(error => { + console.error('Error updating subtask status:', error); + showNotification('Error updating subtask status', 'error'); + renderSubtasks(); // Re-render to revert checkbox + }); + } else { + currentSubtasks[index].status = newStatus; + } +} + +// Save all subtasks for a task +function saveSubtasks(taskId) { + const promises = []; + + currentSubtasks.forEach(subtask => { + if (subtask.isNew && subtask.name.trim()) { + // Create new subtask + promises.push( + fetch('/api/subtasks', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + task_id: taskId, + name: subtask.name, + priority: subtask.priority, + assigned_to_id: subtask.assigned_to_id, + status: subtask.status + }) + }) + .then(response => response.json()) + ); + } else if (subtask.id && !subtask.isNew) { + // Update existing subtask + promises.push( + fetch(`/api/subtasks/${subtask.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: subtask.name, + priority: subtask.priority, + assigned_to_id: subtask.assigned_to_id, + status: subtask.status + }) + }) + .then(response => response.json()) + ); + } + }); + + return Promise.all(promises); +} + +// Update task progress based on subtasks +function updateTaskProgress() { + const taskId = document.getElementById('task-id').value; + if (!taskId) return; + + // Refresh the task card in the board + if (typeof refreshTaskCard === 'function') { + refreshTaskCard(taskId); + } +} + +// Render assignee options +function renderAssigneeOptions(selectedId) { + // This should be populated from the global team members list + const teamMembers = window.teamMembers || []; + return teamMembers.map(member => + `` + ).join(''); +} + +// Helper function to escape HTML +function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +// Display subtasks in task cards +function renderSubtasksInCard(subtasks) { + if (!subtasks || subtasks.length === 0) return ''; + + const completedCount = subtasks.filter(s => s.status === 'COMPLETED').length; + const totalCount = subtasks.length; + const percentage = Math.round((completedCount / totalCount) * 100); + + return ` +
+
+
+
+
+ ${completedCount}/${totalCount} subtasks +
+
+ `; +} + +// Add subtask styles +const subtaskStyles = ` + +`; + +// Inject styles when document is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + document.head.insertAdjacentHTML('beforeend', subtaskStyles); + }); +} else { + document.head.insertAdjacentHTML('beforeend', subtaskStyles); +} \ No newline at end of file diff --git a/templates/task_modal.html b/templates/task_modal.html index 33f6055..f65d024 100644 --- a/templates/task_modal.html +++ b/templates/task_modal.html @@ -30,7 +30,7 @@
- +
@@ -210,21 +210,30 @@ document.addEventListener('DOMContentLoaded', function() {