912 lines
31 KiB
HTML
912 lines
31 KiB
HTML
{% extends "layout.html" %}
|
|
|
|
{% block content %}
|
|
<div class="management-container task-management-container">
|
|
<!-- Header Section -->
|
|
<div class="management-header task-header">
|
|
<h1>📋 Task Management</h1>
|
|
<div class="management-controls task-controls">
|
|
<!-- View Switcher -->
|
|
<div class="view-switcher">
|
|
<button class="view-btn active" data-view="all">All Tasks</button>
|
|
<button class="view-btn" data-view="project">By Project</button>
|
|
<button class="view-btn" data-view="personal">My Tasks</button>
|
|
{% if g.user.team_id %}
|
|
<button class="view-btn" data-view="team">Team Tasks</button>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="task-filters">
|
|
<select id="project-filter" class="filter-select">
|
|
<option value="">All Projects</option>
|
|
{% for project in available_projects %}
|
|
<option value="{{ project.id }}">{{ project.code }} - {{ project.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
|
|
<select id="assignee-filter" class="filter-select">
|
|
<option value="">All Assignees</option>
|
|
<option value="unassigned">Unassigned</option>
|
|
{% for user in team_members %}
|
|
<option value="{{ user.id }}">{{ user.username }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
|
|
<select id="priority-filter" class="filter-select">
|
|
<option value="">All Priorities</option>
|
|
<option value="URGENT">Urgent</option>
|
|
<option value="HIGH">High</option>
|
|
<option value="MEDIUM">Medium</option>
|
|
<option value="LOW">Low</option>
|
|
</select>
|
|
|
|
<!-- Date Range Filters -->
|
|
<div class="date-range-filters">
|
|
<label for="start-date-filter">From:</label>
|
|
<input type="date" id="start-date-filter" class="filter-input">
|
|
|
|
<label for="end-date-filter">To:</label>
|
|
<input type="date" id="end-date-filter" class="filter-input">
|
|
|
|
<select id="date-field-filter" class="filter-select">
|
|
<option value="created">Created Date</option>
|
|
<option value="due">Due Date</option>
|
|
<option value="completed">Completed Date</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Sprint Filter -->
|
|
<select id="sprint-filter" class="filter-select">
|
|
<option value="">All Sprints</option>
|
|
<option value="no-sprint">No Sprint</option>
|
|
<option value="current">Current Sprint</option>
|
|
<!-- Sprint options will be populated dynamically -->
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="management-actions task-actions">
|
|
<button id="add-task-btn" class="btn btn-primary">+ Add Task</button>
|
|
<button id="refresh-tasks" class="btn btn-secondary">🔄 Refresh</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Task Statistics -->
|
|
<div class="management-stats task-stats">
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="total-tasks">0</div>
|
|
<div class="stat-label">Total Tasks</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="completed-tasks">0</div>
|
|
<div class="stat-label">Completed</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="in-progress-tasks">0</div>
|
|
<div class="stat-label">In Progress</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="overdue-tasks">0</div>
|
|
<div class="stat-label">Overdue</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Kanban Board -->
|
|
<div class="kanban-board" id="task-board">
|
|
<div class="kanban-column" data-status="NOT_STARTED">
|
|
<div class="column-header">
|
|
<h3>📝 Not Started</h3>
|
|
<span class="task-count">0</span>
|
|
</div>
|
|
<div class="column-content" id="column-NOT_STARTED">
|
|
<!-- Task cards will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="kanban-column" data-status="IN_PROGRESS">
|
|
<div class="column-header">
|
|
<h3>⚡ In Progress</h3>
|
|
<span class="task-count">0</span>
|
|
</div>
|
|
<div class="column-content" id="column-IN_PROGRESS">
|
|
<!-- Task cards will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="kanban-column" data-status="ON_HOLD">
|
|
<div class="column-header">
|
|
<h3>⏸️ On Hold</h3>
|
|
<span class="task-count">0</span>
|
|
</div>
|
|
<div class="column-content" id="column-ON_HOLD">
|
|
<!-- Task cards will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="kanban-column" data-status="COMPLETED">
|
|
<div class="column-header">
|
|
<h3>✅ Completed</h3>
|
|
<span class="task-count">0</span>
|
|
</div>
|
|
<div class="column-content" id="column-COMPLETED">
|
|
<!-- Task cards will be populated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading and Error States -->
|
|
<div id="loading-indicator" class="loading-spinner" style="display: none;">
|
|
<div class="spinner"></div>
|
|
<p>Loading tasks...</p>
|
|
</div>
|
|
|
|
<div id="error-message" class="error-alert" style="display: none;">
|
|
<p>Failed to load tasks. Please try again.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Task Detail Modal -->
|
|
<div id="task-modal" class="modal" style="display: none;">
|
|
<div class="modal-content large">
|
|
<div class="modal-header">
|
|
<h2 id="modal-title">Task Details</h2>
|
|
<span class="close" onclick="closeTaskModal()">×</span>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="task-form">
|
|
<input type="hidden" id="task-id">
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="task-name">Task Name *</label>
|
|
<input type="text" id="task-name" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="task-priority">Priority</label>
|
|
<select id="task-priority">
|
|
<option value="LOW">Low</option>
|
|
<option value="MEDIUM">Medium</option>
|
|
<option value="HIGH">High</option>
|
|
<option value="URGENT">Urgent</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="task-description">Description</label>
|
|
<textarea id="task-description" rows="4"></textarea>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="task-project">Project</label>
|
|
<select id="task-project">
|
|
<option value="">Select Project</option>
|
|
{% for project in available_projects %}
|
|
<option value="{{ project.id }}">{{ project.code }} - {{ project.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="task-assignee">Assigned To</label>
|
|
<select id="task-assignee">
|
|
<option value="">Unassigned</option>
|
|
{% for user in team_members %}
|
|
<option value="{{ user.id }}">{{ user.username }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="task-sprint">Sprint</label>
|
|
<select id="task-sprint">
|
|
<option value="">No Sprint</option>
|
|
<!-- Sprint options will be populated dynamically -->
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="task-estimated-hours">Estimated Hours</label>
|
|
<input type="number" id="task-estimated-hours" min="0" step="0.5">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="task-due-date">Due Date</label>
|
|
<input type="date" id="task-due-date">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="task-estimated-hours">Estimated Hours</label>
|
|
<input type="number" id="task-estimated-hours" min="0" step="0.5">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="task-status">Status</label>
|
|
<select id="task-status">
|
|
<option value="NOT_STARTED">Not Started</option>
|
|
<option value="IN_PROGRESS">In Progress</option>
|
|
<option value="ON_HOLD">On Hold</option>
|
|
<option value="COMPLETED">Completed</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Subtasks Section -->
|
|
<div class="subtasks-section">
|
|
<h3>Subtasks</h3>
|
|
<div id="subtasks-container">
|
|
<!-- Subtasks will be populated here -->
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-secondary" onclick="addSubtask()">+ Add Subtask</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick="closeTaskModal()">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="saveTask()">Save Task</button>
|
|
<button type="button" class="btn btn-danger" onclick="deleteTask()" id="delete-task-btn" style="display: none;">Delete Task</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Styles -->
|
|
<style>
|
|
/* Task-specific filters */
|
|
.task-filters {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.filter-select {
|
|
padding: 0.5rem;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.filter-input {
|
|
padding: 0.5rem;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.date-range-filters {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
background: #f8f9fa;
|
|
padding: 0.5rem;
|
|
border-radius: 6px;
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
.date-range-filters label {
|
|
font-size: 0.875rem;
|
|
color: #666;
|
|
margin: 0;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.kanban-board {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
gap: 1rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.kanban-column {
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
padding: 1rem;
|
|
min-height: 500px;
|
|
}
|
|
|
|
.column-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 2px solid #ddd;
|
|
}
|
|
|
|
.column-header h3 {
|
|
margin: 0;
|
|
font-size: 1.1rem;
|
|
color: #333;
|
|
}
|
|
|
|
.task-count {
|
|
background: #007bff;
|
|
color: white;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 12px;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.column-content {
|
|
min-height: 400px;
|
|
padding: 0.5rem 0;
|
|
}
|
|
|
|
.task-card {
|
|
background: white;
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
padding: 1rem;
|
|
margin-bottom: 0.75rem;
|
|
cursor: pointer;
|
|
transition: box-shadow 0.2s, transform 0.1s;
|
|
}
|
|
|
|
.task-card:hover {
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.task-card.dragging {
|
|
opacity: 0.5;
|
|
transform: rotate(5deg);
|
|
}
|
|
|
|
.task-card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.task-title {
|
|
font-weight: bold;
|
|
margin: 0;
|
|
color: #333;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
|
|
.task-project {
|
|
font-size: 0.8rem;
|
|
color: #666;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.task-meta {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
font-size: 0.8rem;
|
|
color: #666;
|
|
}
|
|
|
|
.task-assignee {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.task-due-date {
|
|
color: #666;
|
|
}
|
|
|
|
.task-due-date.overdue {
|
|
color: #dc3545;
|
|
font-weight: bold;
|
|
}
|
|
|
|
|
|
|
|
.subtasks-section {
|
|
margin-top: 1.5rem;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid #eee;
|
|
}
|
|
|
|
.subtasks-section h3 {
|
|
margin-bottom: 1rem;
|
|
color: #333;
|
|
}
|
|
|
|
.subtask-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
padding: 0.5rem;
|
|
background: #f8f9fa;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.subtask-item input[type="text"] {
|
|
flex: 1;
|
|
margin: 0;
|
|
}
|
|
|
|
.subtask-item button {
|
|
padding: 0.25rem 0.5rem;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.kanban-board {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<!-- SortableJS for drag and drop -->
|
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
|
|
|
<script>
|
|
// Task Management Controller
|
|
class UnifiedTaskManager {
|
|
constructor() {
|
|
this.tasks = [];
|
|
this.currentView = 'all';
|
|
this.filters = {
|
|
project: '',
|
|
assignee: '',
|
|
priority: '',
|
|
startDate: '',
|
|
endDate: '',
|
|
dateField: 'created',
|
|
sprint: ''
|
|
};
|
|
this.currentTask = null;
|
|
this.sortableInstances = [];
|
|
}
|
|
|
|
async init() {
|
|
this.setupEventListeners();
|
|
this.setupSortable();
|
|
await this.loadSprints();
|
|
await this.loadTasks();
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// View switcher
|
|
document.querySelectorAll('.view-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
this.switchView(e.target.dataset.view);
|
|
});
|
|
});
|
|
|
|
// Filters
|
|
document.getElementById('project-filter').addEventListener('change', () => {
|
|
this.filters.project = document.getElementById('project-filter').value;
|
|
this.applyFilters();
|
|
});
|
|
|
|
document.getElementById('assignee-filter').addEventListener('change', () => {
|
|
this.filters.assignee = document.getElementById('assignee-filter').value;
|
|
this.applyFilters();
|
|
});
|
|
|
|
document.getElementById('priority-filter').addEventListener('change', () => {
|
|
this.filters.priority = document.getElementById('priority-filter').value;
|
|
this.applyFilters();
|
|
});
|
|
|
|
// Date range filters
|
|
document.getElementById('start-date-filter').addEventListener('change', () => {
|
|
this.filters.startDate = document.getElementById('start-date-filter').value;
|
|
this.applyFilters();
|
|
});
|
|
|
|
document.getElementById('end-date-filter').addEventListener('change', () => {
|
|
this.filters.endDate = document.getElementById('end-date-filter').value;
|
|
this.applyFilters();
|
|
});
|
|
|
|
document.getElementById('date-field-filter').addEventListener('change', () => {
|
|
this.filters.dateField = document.getElementById('date-field-filter').value;
|
|
this.applyFilters();
|
|
});
|
|
|
|
// Sprint filter
|
|
document.getElementById('sprint-filter').addEventListener('change', () => {
|
|
this.filters.sprint = document.getElementById('sprint-filter').value;
|
|
this.applyFilters();
|
|
});
|
|
|
|
// Actions
|
|
document.getElementById('add-task-btn').addEventListener('click', () => {
|
|
this.openTaskModal();
|
|
});
|
|
|
|
document.getElementById('refresh-tasks').addEventListener('click', () => {
|
|
this.loadTasks();
|
|
});
|
|
}
|
|
|
|
setupSortable() {
|
|
const columns = document.querySelectorAll('.column-content');
|
|
columns.forEach(column => {
|
|
const sortable = Sortable.create(column, {
|
|
group: 'tasks',
|
|
animation: 150,
|
|
onEnd: (evt) => {
|
|
this.handleTaskMove(evt);
|
|
}
|
|
});
|
|
this.sortableInstances.push(sortable);
|
|
});
|
|
}
|
|
|
|
switchView(view) {
|
|
// Update button states
|
|
document.querySelectorAll('.view-btn').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
document.querySelector(`[data-view="${view}"]`).classList.add('active');
|
|
|
|
this.currentView = view;
|
|
this.applyFilters();
|
|
}
|
|
|
|
async loadSprints() {
|
|
try {
|
|
const response = await fetch('/api/sprints');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
const sprintFilter = document.getElementById('sprint-filter');
|
|
|
|
// Clear existing options except the default ones
|
|
const defaultOptions = Array.from(sprintFilter.children).slice(0, 3);
|
|
sprintFilter.innerHTML = '';
|
|
defaultOptions.forEach(option => sprintFilter.appendChild(option));
|
|
|
|
// Add sprint options
|
|
data.sprints.forEach(sprint => {
|
|
const option = document.createElement('option');
|
|
option.value = sprint.id;
|
|
option.textContent = `${sprint.name} (${sprint.status})`;
|
|
sprintFilter.appendChild(option);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading sprints:', error);
|
|
}
|
|
}
|
|
|
|
async loadTasks() {
|
|
document.getElementById('loading-indicator').style.display = 'flex';
|
|
document.getElementById('error-message').style.display = 'none';
|
|
|
|
try {
|
|
const response = await fetch('/api/tasks/unified');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
this.tasks = data.tasks;
|
|
this.renderTasks();
|
|
this.updateStatistics();
|
|
} else {
|
|
throw new Error(data.message || 'Failed to load tasks');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading tasks:', error);
|
|
document.getElementById('error-message').style.display = 'block';
|
|
} finally {
|
|
document.getElementById('loading-indicator').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
renderTasks() {
|
|
// Clear all columns
|
|
document.querySelectorAll('.column-content').forEach(column => {
|
|
column.innerHTML = '';
|
|
});
|
|
|
|
// Filter tasks based on current view and filters
|
|
const filteredTasks = this.getFilteredTasks();
|
|
|
|
// Group tasks by status
|
|
const tasksByStatus = {
|
|
'NOT_STARTED': [],
|
|
'IN_PROGRESS': [],
|
|
'ON_HOLD': [],
|
|
'COMPLETED': []
|
|
};
|
|
|
|
filteredTasks.forEach(task => {
|
|
if (tasksByStatus[task.status]) {
|
|
tasksByStatus[task.status].push(task);
|
|
}
|
|
});
|
|
|
|
// Render tasks in each column
|
|
Object.keys(tasksByStatus).forEach(status => {
|
|
const column = document.getElementById(`column-${status}`);
|
|
const tasks = tasksByStatus[status];
|
|
|
|
tasks.forEach(task => {
|
|
const taskCard = this.createTaskCard(task);
|
|
column.appendChild(taskCard);
|
|
});
|
|
|
|
// Update task count
|
|
const countElement = column.parentElement.querySelector('.task-count');
|
|
countElement.textContent = tasks.length;
|
|
});
|
|
}
|
|
|
|
getFilteredTasks() {
|
|
return this.tasks.filter(task => {
|
|
// View filter
|
|
if (this.currentView === 'personal' && task.assigned_to_id !== {{ g.user.id }}) {
|
|
return false;
|
|
}
|
|
if (this.currentView === 'team' && !task.is_team_task) {
|
|
return false;
|
|
}
|
|
if (this.currentView === 'project' && !task.project_id) {
|
|
return false;
|
|
}
|
|
|
|
// Project filter
|
|
if (this.filters.project && task.project_id != this.filters.project) {
|
|
return false;
|
|
}
|
|
|
|
// Assignee filter
|
|
if (this.filters.assignee) {
|
|
if (this.filters.assignee === 'unassigned' && task.assigned_to_id) {
|
|
return false;
|
|
}
|
|
if (this.filters.assignee !== 'unassigned' && task.assigned_to_id != this.filters.assignee) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Priority filter
|
|
if (this.filters.priority && task.priority !== this.filters.priority) {
|
|
return false;
|
|
}
|
|
|
|
// Date range filter
|
|
if (this.filters.startDate || this.filters.endDate) {
|
|
let taskDate = null;
|
|
|
|
switch (this.filters.dateField) {
|
|
case 'created':
|
|
taskDate = task.created_at ? new Date(task.created_at) : null;
|
|
break;
|
|
case 'due':
|
|
taskDate = task.due_date ? new Date(task.due_date) : null;
|
|
break;
|
|
case 'completed':
|
|
taskDate = task.completed_date ? new Date(task.completed_date) : null;
|
|
break;
|
|
}
|
|
|
|
if (taskDate) {
|
|
if (this.filters.startDate && taskDate < new Date(this.filters.startDate)) {
|
|
return false;
|
|
}
|
|
if (this.filters.endDate && taskDate > new Date(this.filters.endDate)) {
|
|
return false;
|
|
}
|
|
} else if (this.filters.startDate || this.filters.endDate) {
|
|
// If we're filtering by date but task has no date in the selected field, exclude it
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Sprint filter
|
|
if (this.filters.sprint) {
|
|
if (this.filters.sprint === 'no-sprint' && task.sprint_id) {
|
|
return false;
|
|
}
|
|
if (this.filters.sprint === 'current' && !task.is_current_sprint) {
|
|
return false;
|
|
}
|
|
if (this.filters.sprint !== 'no-sprint' && this.filters.sprint !== 'current' &&
|
|
task.sprint_id != this.filters.sprint) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
createTaskCard(task) {
|
|
const card = document.createElement('div');
|
|
card.className = 'management-card task-card';
|
|
card.dataset.taskId = task.id;
|
|
card.addEventListener('click', () => this.openTaskModal(task));
|
|
|
|
const dueDate = task.due_date ? new Date(task.due_date) : null;
|
|
const isOverdue = dueDate && dueDate < new Date() && task.status !== 'COMPLETED';
|
|
|
|
card.innerHTML = `
|
|
<div class="task-card-header">
|
|
<h4 class="task-title">${task.name}</h4>
|
|
<span class="priority-badge ${task.priority}">${task.priority}</span>
|
|
</div>
|
|
${task.project_name ? `<div class="task-project">${task.project_code} - ${task.project_name}</div>` : ''}
|
|
<div class="task-meta">
|
|
<span class="task-assignee">${task.assigned_to_name || 'Unassigned'}</span>
|
|
${dueDate ? `<span class="task-due-date ${isOverdue ? 'overdue' : ''}">${dueDate.toLocaleDateString()}</span>` : ''}
|
|
</div>
|
|
`;
|
|
|
|
return card;
|
|
}
|
|
|
|
applyFilters() {
|
|
this.renderTasks();
|
|
}
|
|
|
|
updateStatistics() {
|
|
const total = this.tasks.length;
|
|
const completed = this.tasks.filter(t => t.status === 'COMPLETED').length;
|
|
const inProgress = this.tasks.filter(t => t.status === 'IN_PROGRESS').length;
|
|
const overdue = this.tasks.filter(t => {
|
|
const dueDate = t.due_date ? new Date(t.due_date) : null;
|
|
return dueDate && dueDate < new Date() && t.status !== 'COMPLETED';
|
|
}).length;
|
|
|
|
document.getElementById('total-tasks').textContent = total;
|
|
document.getElementById('completed-tasks').textContent = completed;
|
|
document.getElementById('in-progress-tasks').textContent = inProgress;
|
|
document.getElementById('overdue-tasks').textContent = overdue;
|
|
}
|
|
|
|
async handleTaskMove(evt) {
|
|
const taskId = evt.item.dataset.taskId;
|
|
const newStatus = evt.to.id.replace('column-', '');
|
|
|
|
try {
|
|
const response = await fetch(`/api/tasks/${taskId}/status`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ status: newStatus })
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
// Update local task data
|
|
const task = this.tasks.find(t => t.id == taskId);
|
|
if (task) {
|
|
task.status = newStatus;
|
|
this.updateStatistics();
|
|
}
|
|
} else {
|
|
throw new Error(data.message || 'Failed to update task');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating task status:', error);
|
|
// Revert the move
|
|
this.renderTasks();
|
|
}
|
|
}
|
|
|
|
openTaskModal(task = null) {
|
|
this.currentTask = task;
|
|
const modal = document.getElementById('task-modal');
|
|
|
|
if (task) {
|
|
document.getElementById('modal-title').textContent = 'Edit Task';
|
|
document.getElementById('task-id').value = task.id;
|
|
document.getElementById('task-name').value = task.name;
|
|
document.getElementById('task-description').value = task.description || '';
|
|
document.getElementById('task-priority').value = task.priority;
|
|
document.getElementById('task-project').value = task.project_id || '';
|
|
document.getElementById('task-assignee').value = task.assigned_to_id || '';
|
|
document.getElementById('task-due-date').value = task.due_date || '';
|
|
document.getElementById('task-estimated-hours').value = task.estimated_hours || '';
|
|
document.getElementById('task-status').value = task.status;
|
|
document.getElementById('delete-task-btn').style.display = 'inline-block';
|
|
} else {
|
|
document.getElementById('modal-title').textContent = 'Add New Task';
|
|
document.getElementById('task-form').reset();
|
|
document.getElementById('task-id').value = '';
|
|
document.getElementById('delete-task-btn').style.display = 'none';
|
|
}
|
|
|
|
modal.style.display = 'block';
|
|
}
|
|
|
|
async saveTask() {
|
|
const taskData = {
|
|
name: document.getElementById('task-name').value,
|
|
description: document.getElementById('task-description').value,
|
|
priority: document.getElementById('task-priority').value,
|
|
project_id: document.getElementById('task-project').value || null,
|
|
assigned_to_id: document.getElementById('task-assignee').value || null,
|
|
due_date: document.getElementById('task-due-date').value || null,
|
|
estimated_hours: document.getElementById('task-estimated-hours').value || null,
|
|
status: document.getElementById('task-status').value
|
|
};
|
|
|
|
const taskId = document.getElementById('task-id').value;
|
|
const isEdit = taskId !== '';
|
|
|
|
try {
|
|
const response = await fetch(`/api/tasks${isEdit ? `/${taskId}` : ''}`, {
|
|
method: isEdit ? 'PUT' : 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(taskData)
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
closeTaskModal();
|
|
await this.loadTasks();
|
|
} else {
|
|
throw new Error(data.message || 'Failed to save task');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving task:', error);
|
|
alert('Failed to save task: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async deleteTask() {
|
|
if (!this.currentTask) return;
|
|
|
|
if (confirm('Are you sure you want to delete this task?')) {
|
|
try {
|
|
const response = await fetch(`/api/tasks/${this.currentTask.id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
closeTaskModal();
|
|
await this.loadTasks();
|
|
} else {
|
|
throw new Error(data.message || 'Failed to delete task');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting task:', error);
|
|
alert('Failed to delete task: ' + error.message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Global functions
|
|
let taskManager;
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
taskManager = new UnifiedTaskManager();
|
|
taskManager.init();
|
|
});
|
|
|
|
function closeTaskModal() {
|
|
document.getElementById('task-modal').style.display = 'none';
|
|
taskManager.currentTask = null;
|
|
}
|
|
|
|
function saveTask() {
|
|
taskManager.saveTask();
|
|
}
|
|
|
|
function deleteTask() {
|
|
taskManager.deleteTask();
|
|
}
|
|
|
|
function addSubtask() {
|
|
// TODO: Implement subtask functionality
|
|
}
|
|
|
|
// Close modal when clicking outside
|
|
window.onclick = function(event) {
|
|
const modal = document.getElementById('task-modal');
|
|
if (event.target === modal) {
|
|
closeTaskModal();
|
|
}
|
|
};
|
|
</script>
|
|
{% endblock %} |