Add Task Archive feature.
This commit is contained in:
88
app.py
88
app.py
@@ -69,6 +69,15 @@ def run_migrations():
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
init_system_settings()
|
||||
|
||||
# Run PostgreSQL-specific migrations
|
||||
try:
|
||||
from migrate_db import migrate_postgresql_schema
|
||||
migrate_postgresql_schema()
|
||||
except ImportError:
|
||||
print("PostgreSQL migration function not available")
|
||||
except Exception as e:
|
||||
print(f"Warning: PostgreSQL migration failed: {e}")
|
||||
print("PostgreSQL setup completed successfully!")
|
||||
else:
|
||||
print("Using SQLite - running SQLite migrations...")
|
||||
@@ -3996,6 +4005,13 @@ def update_task_status(task_id):
|
||||
# Clear completion date if moving away from completed
|
||||
task.completed_date = None
|
||||
|
||||
# Set archived date if status is ARCHIVED
|
||||
if task_status == TaskStatus.ARCHIVED:
|
||||
task.archived_date = datetime.now().date()
|
||||
elif old_status == TaskStatus.ARCHIVED:
|
||||
# Clear archived date if moving away from archived
|
||||
task.archived_date = None
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
@@ -4199,6 +4215,78 @@ def remove_task_dependency(task_id, dependency_task_id):
|
||||
return jsonify({'success': False, 'message': str(e)})
|
||||
|
||||
|
||||
# Task Archive/Restore APIs
|
||||
@app.route('/api/tasks/<int:task_id>/archive', methods=['POST'])
|
||||
@role_required(Role.TEAM_MEMBER)
|
||||
@company_required
|
||||
def archive_task(task_id):
|
||||
"""Archive a completed task"""
|
||||
try:
|
||||
# Get the task and verify ownership through project
|
||||
task = Task.query.join(Project).filter(
|
||||
Task.id == task_id,
|
||||
Project.company_id == g.user.company_id
|
||||
).first()
|
||||
if not task:
|
||||
return jsonify({'success': False, 'message': 'Task not found'})
|
||||
|
||||
# Only allow archiving completed tasks
|
||||
if task.status != TaskStatus.COMPLETED:
|
||||
return jsonify({'success': False, 'message': 'Only completed tasks can be archived'})
|
||||
|
||||
# Archive the task
|
||||
task.status = TaskStatus.ARCHIVED
|
||||
task.archived_date = datetime.now().date()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Task archived successfully',
|
||||
'archived_date': task.archived_date.isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error archiving task: {str(e)}")
|
||||
return jsonify({'success': False, 'message': str(e)})
|
||||
|
||||
|
||||
@app.route('/api/tasks/<int:task_id>/restore', methods=['POST'])
|
||||
@role_required(Role.TEAM_MEMBER)
|
||||
@company_required
|
||||
def restore_task(task_id):
|
||||
"""Restore an archived task to completed status"""
|
||||
try:
|
||||
# Get the task and verify ownership through project
|
||||
task = Task.query.join(Project).filter(
|
||||
Task.id == task_id,
|
||||
Project.company_id == g.user.company_id
|
||||
).first()
|
||||
if not task:
|
||||
return jsonify({'success': False, 'message': 'Task not found'})
|
||||
|
||||
# Only allow restoring archived tasks
|
||||
if task.status != TaskStatus.ARCHIVED:
|
||||
return jsonify({'success': False, 'message': 'Only archived tasks can be restored'})
|
||||
|
||||
# Restore the task to completed status
|
||||
task.status = TaskStatus.COMPLETED
|
||||
task.archived_date = None
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Task restored successfully'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error restoring task: {str(e)}")
|
||||
return jsonify({'success': False, 'message': str(e)})
|
||||
|
||||
|
||||
# Sprint Management APIs
|
||||
@app.route('/api/sprints')
|
||||
@role_required(Role.TEAM_MEMBER)
|
||||
|
||||
203
migrate_db.py
203
migrate_db.py
@@ -74,6 +74,10 @@ def run_all_migrations(db_path=None):
|
||||
migrate_task_system(db_path)
|
||||
migrate_system_events(db_path)
|
||||
migrate_dashboard_system(db_path)
|
||||
|
||||
# Run PostgreSQL-specific migrations if applicable
|
||||
if FLASK_AVAILABLE:
|
||||
migrate_postgresql_schema()
|
||||
|
||||
if FLASK_AVAILABLE:
|
||||
with app.app_context():
|
||||
@@ -604,24 +608,61 @@ def migrate_task_system(db_path):
|
||||
cursor.execute("""
|
||||
CREATE TABLE task (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_number VARCHAR(20) NOT NULL UNIQUE,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
status VARCHAR(50) DEFAULT 'Not Started',
|
||||
priority VARCHAR(50) DEFAULT 'Medium',
|
||||
estimated_hours FLOAT,
|
||||
project_id INTEGER NOT NULL,
|
||||
sprint_id INTEGER,
|
||||
assigned_to_id INTEGER,
|
||||
start_date DATE,
|
||||
due_date DATE,
|
||||
completed_date DATE,
|
||||
archived_date DATE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (project_id) REFERENCES project (id),
|
||||
FOREIGN KEY (sprint_id) REFERENCES sprint (id),
|
||||
FOREIGN KEY (assigned_to_id) REFERENCES user (id),
|
||||
FOREIGN KEY (created_by_id) REFERENCES user (id)
|
||||
)
|
||||
""")
|
||||
else:
|
||||
# Add missing columns to existing task table
|
||||
cursor.execute("PRAGMA table_info(task)")
|
||||
task_columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
task_migrations = [
|
||||
('task_number', "ALTER TABLE task ADD COLUMN task_number VARCHAR(20)"),
|
||||
('sprint_id', "ALTER TABLE task ADD COLUMN sprint_id INTEGER"),
|
||||
('archived_date', "ALTER TABLE task ADD COLUMN archived_date DATE")
|
||||
]
|
||||
|
||||
for column_name, sql_command in task_migrations:
|
||||
if column_name not in task_columns:
|
||||
print(f"Adding {column_name} column to task table...")
|
||||
cursor.execute(sql_command)
|
||||
|
||||
# Add unique constraint for task_number if it was just added
|
||||
if 'task_number' not in task_columns:
|
||||
print("Adding unique constraint for task_number...")
|
||||
# For SQLite, we need to recreate the table to add unique constraint
|
||||
cursor.execute("CREATE UNIQUE INDEX idx_task_number ON task(task_number)")
|
||||
|
||||
# Generate task numbers for existing tasks that don't have them
|
||||
print("Generating task numbers for existing tasks...")
|
||||
cursor.execute("SELECT id FROM task WHERE task_number IS NULL ORDER BY id")
|
||||
tasks_without_numbers = cursor.fetchall()
|
||||
|
||||
for i, (task_id,) in enumerate(tasks_without_numbers, 1):
|
||||
task_number = f"TSK-{i:03d}"
|
||||
cursor.execute("UPDATE task SET task_number = ? WHERE id = ?", (task_number, task_id))
|
||||
|
||||
if tasks_without_numbers:
|
||||
print(f"Generated {len(tasks_without_numbers)} task numbers")
|
||||
|
||||
# Check if sub_task table exists
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='sub_task'")
|
||||
@@ -649,6 +690,49 @@ def migrate_task_system(db_path):
|
||||
)
|
||||
""")
|
||||
|
||||
# Check if task_dependency table exists
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='task_dependency'")
|
||||
if not cursor.fetchone():
|
||||
print("Creating task_dependency table...")
|
||||
cursor.execute("""
|
||||
CREATE TABLE task_dependency (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
blocked_task_id INTEGER NOT NULL,
|
||||
blocking_task_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (blocked_task_id) REFERENCES task (id),
|
||||
FOREIGN KEY (blocking_task_id) REFERENCES task (id),
|
||||
UNIQUE(blocked_task_id, blocking_task_id),
|
||||
CHECK (blocked_task_id != blocking_task_id)
|
||||
)
|
||||
""")
|
||||
|
||||
# Check if sprint table exists
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='sprint'")
|
||||
if not cursor.fetchone():
|
||||
print("Creating sprint table...")
|
||||
cursor.execute("""
|
||||
CREATE TABLE sprint (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
status VARCHAR(50) DEFAULT 'PLANNING',
|
||||
goal TEXT,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
capacity_hours INTEGER,
|
||||
project_id INTEGER,
|
||||
company_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (project_id) REFERENCES project (id),
|
||||
FOREIGN KEY (company_id) REFERENCES company (id),
|
||||
FOREIGN KEY (created_by_id) REFERENCES user (id),
|
||||
UNIQUE(company_id, name)
|
||||
)
|
||||
""")
|
||||
|
||||
# Add category_id to project table if it doesn't exist
|
||||
cursor.execute("PRAGMA table_info(project)")
|
||||
project_columns = [column[1] for column in cursor.fetchall()]
|
||||
@@ -884,6 +968,120 @@ def create_all_tables(cursor):
|
||||
print("All tables created")
|
||||
|
||||
|
||||
def migrate_postgresql_schema():
|
||||
"""Migrate PostgreSQL schema for archive functionality."""
|
||||
if not FLASK_AVAILABLE:
|
||||
print("Skipping PostgreSQL migration - Flask not available")
|
||||
return
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
from sqlalchemy import text
|
||||
|
||||
with app.app_context():
|
||||
# Check if we're using PostgreSQL
|
||||
database_url = app.config['SQLALCHEMY_DATABASE_URI']
|
||||
if not ('postgresql://' in database_url or 'postgres://' in database_url):
|
||||
print("Not using PostgreSQL - skipping PostgreSQL migration")
|
||||
return
|
||||
|
||||
print("Running PostgreSQL schema migrations...")
|
||||
|
||||
# Check if archived_date column exists
|
||||
result = db.session.execute(text("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'task' AND column_name = 'archived_date'
|
||||
"""))
|
||||
|
||||
if not result.fetchone():
|
||||
print("Adding archived_date column to task table...")
|
||||
db.session.execute(text("ALTER TABLE task ADD COLUMN archived_date DATE"))
|
||||
db.session.commit()
|
||||
|
||||
# Check if ARCHIVED status exists in enum
|
||||
result = db.session.execute(text("""
|
||||
SELECT enumlabel
|
||||
FROM pg_enum
|
||||
WHERE enumtypid = (SELECT oid FROM pg_type WHERE typname = 'taskstatus')
|
||||
AND enumlabel = 'ARCHIVED'
|
||||
"""))
|
||||
|
||||
if not result.fetchone():
|
||||
print("Adding ARCHIVED status to TaskStatus enum...")
|
||||
db.session.execute(text("ALTER TYPE taskstatus ADD VALUE 'ARCHIVED'"))
|
||||
db.session.commit()
|
||||
|
||||
# Check if task_number column exists
|
||||
result = db.session.execute(text("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'task' AND column_name = 'task_number'
|
||||
"""))
|
||||
|
||||
if not result.fetchone():
|
||||
print("Adding task_number column to task table...")
|
||||
db.session.execute(text("ALTER TABLE task ADD COLUMN task_number VARCHAR(20) UNIQUE"))
|
||||
|
||||
# Generate task numbers for existing tasks
|
||||
print("Generating task numbers for existing tasks...")
|
||||
result = db.session.execute(text("SELECT id FROM task WHERE task_number IS NULL ORDER BY id"))
|
||||
tasks_without_numbers = result.fetchall()
|
||||
|
||||
for i, (task_id,) in enumerate(tasks_without_numbers, 1):
|
||||
task_number = f"TSK-{i:03d}"
|
||||
db.session.execute(text("UPDATE task SET task_number = :task_number WHERE id = :task_id"),
|
||||
{"task_number": task_number, "task_id": task_id})
|
||||
|
||||
db.session.commit()
|
||||
if tasks_without_numbers:
|
||||
print(f"Generated {len(tasks_without_numbers)} task numbers")
|
||||
|
||||
# Check if sprint_id column exists
|
||||
result = db.session.execute(text("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'task' AND column_name = 'sprint_id'
|
||||
"""))
|
||||
|
||||
if not result.fetchone():
|
||||
print("Adding sprint_id column to task table...")
|
||||
db.session.execute(text("ALTER TABLE task ADD COLUMN sprint_id INTEGER"))
|
||||
db.session.execute(text("ALTER TABLE task ADD CONSTRAINT fk_task_sprint FOREIGN KEY (sprint_id) REFERENCES sprint (id)"))
|
||||
db.session.commit()
|
||||
|
||||
# Check if task_dependency table exists
|
||||
result = db.session.execute(text("""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_name = 'task_dependency'
|
||||
"""))
|
||||
|
||||
if not result.fetchone():
|
||||
print("Creating task_dependency table...")
|
||||
db.session.execute(text("""
|
||||
CREATE TABLE task_dependency (
|
||||
id SERIAL PRIMARY KEY,
|
||||
blocked_task_id INTEGER NOT NULL,
|
||||
blocking_task_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (blocked_task_id) REFERENCES task (id),
|
||||
FOREIGN KEY (blocking_task_id) REFERENCES task (id),
|
||||
UNIQUE(blocked_task_id, blocking_task_id),
|
||||
CHECK (blocked_task_id <> blocking_task_id)
|
||||
)
|
||||
"""))
|
||||
db.session.commit()
|
||||
|
||||
print("PostgreSQL schema migration completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during PostgreSQL migration: {e}")
|
||||
if FLASK_AVAILABLE:
|
||||
db.session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
def migrate_dashboard_system(db_file=None):
|
||||
"""Migrate to add Dashboard widget system."""
|
||||
db_path = get_db_path(db_file)
|
||||
@@ -1045,6 +1243,8 @@ def main():
|
||||
help='Run only system events migration')
|
||||
parser.add_argument('--dashboard', '--dash', action='store_true',
|
||||
help='Run only dashboard system migration')
|
||||
parser.add_argument('--postgresql', '--pg', action='store_true',
|
||||
help='Run only PostgreSQL schema migration')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -1081,6 +1281,9 @@ def main():
|
||||
elif args.dashboard:
|
||||
migrate_dashboard_system(db_path)
|
||||
|
||||
elif args.postgresql:
|
||||
migrate_postgresql_schema()
|
||||
|
||||
else:
|
||||
# Default: run all migrations
|
||||
run_all_migrations(db_path)
|
||||
|
||||
@@ -421,6 +421,7 @@ class TaskStatus(enum.Enum):
|
||||
IN_PROGRESS = "In Progress"
|
||||
ON_HOLD = "On Hold"
|
||||
COMPLETED = "Completed"
|
||||
ARCHIVED = "Archived"
|
||||
CANCELLED = "Cancelled"
|
||||
|
||||
# Task priority enumeration
|
||||
@@ -455,6 +456,7 @@ class Task(db.Model):
|
||||
start_date = db.Column(db.Date, nullable=True)
|
||||
due_date = db.Column(db.Date, nullable=True)
|
||||
completed_date = db.Column(db.Date, nullable=True)
|
||||
archived_date = db.Column(db.Date, nullable=True)
|
||||
|
||||
# Metadata
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<option value="IN_PROGRESS">In Progress</option>
|
||||
<option value="ON_HOLD">On Hold</option>
|
||||
<option value="COMPLETED">Completed</option>
|
||||
<option value="ARCHIVED">Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<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>
|
||||
<button id="toggle-archived" class="btn btn-outline" title="Show/Hide Archived Tasks">📦 Show Archived</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,6 +45,10 @@
|
||||
<div class="stat-number" id="overdue-tasks">0</div>
|
||||
<div class="stat-label">Overdue</div>
|
||||
</div>
|
||||
<div class="stat-card" id="archived-stat-card" style="display: none;">
|
||||
<div class="stat-number" id="archived-tasks">0</div>
|
||||
<div class="stat-label">Archived</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Task Board -->
|
||||
@@ -87,6 +92,16 @@
|
||||
<!-- Task cards will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-column archived-column" data-status="ARCHIVED" style="display: none;">
|
||||
<div class="column-header">
|
||||
<h3>📦 Archived</h3>
|
||||
<span class="task-count">0</span>
|
||||
</div>
|
||||
<div class="column-content" id="column-ARCHIVED">
|
||||
<!-- Archived task cards will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading and Error States -->
|
||||
@@ -283,6 +298,88 @@
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Archived Column Styles */
|
||||
.archived-column {
|
||||
background: #f1f3f4;
|
||||
border: 2px dashed #9aa0a6;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.archived-column .column-header {
|
||||
border-bottom-color: #9aa0a6;
|
||||
}
|
||||
|
||||
.archived-column .task-count {
|
||||
background: #9aa0a6;
|
||||
}
|
||||
|
||||
/* Archived Task Card Styles */
|
||||
.archived-column .task-card {
|
||||
opacity: 0.7;
|
||||
border-left: 4px solid #9aa0a6;
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.btn.btn-outline {
|
||||
background: transparent;
|
||||
border: 1px solid #6c757d;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.btn.btn-outline:hover {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn.btn-outline.active {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Task Action Buttons */
|
||||
.task-actions {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.archive-btn, .restore-btn {
|
||||
background: none;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.archive-btn:hover {
|
||||
background: #f8f9fa;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.restore-btn:hover {
|
||||
background: #e3f2fd;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
/* Task Date Displays */
|
||||
.task-completed-date, .task-archived-date {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
margin-top: 0.25rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.task-completed-date {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.task-archived-date {
|
||||
color: #9aa0a6;
|
||||
}
|
||||
|
||||
.column-content {
|
||||
min-height: 400px;
|
||||
padding: 0.5rem 0;
|
||||
@@ -879,6 +976,7 @@ class UnifiedTaskManager {
|
||||
};
|
||||
this.currentTask = null;
|
||||
this.sortableInstances = [];
|
||||
this.showArchived = false;
|
||||
this.currentUserId = {{ g.user.id|tojson }};
|
||||
this.smartSearch = new SmartTaskSearch();
|
||||
this.searchQuery = '';
|
||||
@@ -905,6 +1003,10 @@ class UnifiedTaskManager {
|
||||
document.getElementById('refresh-tasks').addEventListener('click', () => {
|
||||
this.loadTasks();
|
||||
});
|
||||
|
||||
document.getElementById('toggle-archived').addEventListener('click', () => {
|
||||
this.toggleArchivedView();
|
||||
});
|
||||
|
||||
// Date validation
|
||||
document.getElementById('task-due-date').addEventListener('blur', () => {
|
||||
@@ -1287,7 +1389,8 @@ class UnifiedTaskManager {
|
||||
'NOT_STARTED': [],
|
||||
'IN_PROGRESS': [],
|
||||
'ON_HOLD': [],
|
||||
'COMPLETED': []
|
||||
'COMPLETED': [],
|
||||
'ARCHIVED': []
|
||||
};
|
||||
|
||||
filteredTasks.forEach(task => {
|
||||
@@ -1404,8 +1507,24 @@ class UnifiedTaskManager {
|
||||
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';
|
||||
|
||||
const isOverdue = dueDate && dueDate < new Date() && task.status !== 'COMPLETED' && task.status !== 'ARCHIVED';
|
||||
|
||||
// Add archive/restore buttons for completed and archived tasks
|
||||
let actionButtons = '';
|
||||
if (task.status === 'COMPLETED') {
|
||||
actionButtons = `
|
||||
<div class="task-actions">
|
||||
<button class="archive-btn" onclick="taskManager.archiveTask(${task.id}); event.stopPropagation();" title="Archive Task">📦</button>
|
||||
</div>
|
||||
`;
|
||||
} else if (task.status === 'ARCHIVED') {
|
||||
actionButtons = `
|
||||
<div class="task-actions">
|
||||
<button class="restore-btn" onclick="taskManager.restoreTask(${task.id}); event.stopPropagation();" title="Restore Task">↩️</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="task-card-header">
|
||||
<div class="task-title-section">
|
||||
@@ -1419,6 +1538,9 @@ class UnifiedTaskManager {
|
||||
<span class="task-assignee">${task.assigned_to_name || 'Unassigned'}</span>
|
||||
${dueDate ? `<span class="task-due-date ${isOverdue ? 'overdue' : ''}">${formatUserDate(task.due_date)}</span>` : ''}
|
||||
</div>
|
||||
${task.status === 'COMPLETED' ? `<div class="task-completed-date">Completed: ${formatUserDate(task.completed_date)}</div>` : ''}
|
||||
${task.status === 'ARCHIVED' ? `<div class="task-archived-date">Archived: ${formatUserDate(task.archived_date)}</div>` : ''}
|
||||
${actionButtons}
|
||||
`;
|
||||
|
||||
return card;
|
||||
@@ -1432,15 +1554,84 @@ class UnifiedTaskManager {
|
||||
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 archived = this.tasks.filter(t => t.status === 'ARCHIVED').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';
|
||||
return dueDate && dueDate < new Date() && t.status !== 'COMPLETED' && t.status !== 'ARCHIVED';
|
||||
}).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;
|
||||
document.getElementById('archived-tasks').textContent = archived;
|
||||
}
|
||||
|
||||
toggleArchivedView() {
|
||||
this.showArchived = !this.showArchived;
|
||||
const toggleBtn = document.getElementById('toggle-archived');
|
||||
const archivedColumn = document.querySelector('.archived-column');
|
||||
const archivedStatCard = document.getElementById('archived-stat-card');
|
||||
|
||||
if (this.showArchived) {
|
||||
toggleBtn.textContent = '📦 Hide Archived';
|
||||
toggleBtn.classList.add('active');
|
||||
archivedColumn.style.display = 'block';
|
||||
archivedStatCard.style.display = 'block';
|
||||
} else {
|
||||
toggleBtn.textContent = '📦 Show Archived';
|
||||
toggleBtn.classList.remove('active');
|
||||
archivedColumn.style.display = 'none';
|
||||
archivedStatCard.style.display = 'none';
|
||||
}
|
||||
|
||||
this.renderTasks();
|
||||
}
|
||||
|
||||
async archiveTask(taskId) {
|
||||
if (confirm('Are you sure you want to archive this task? It will be moved to the archived section.')) {
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}/archive`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
await this.loadTasks();
|
||||
} else {
|
||||
alert('Failed to archive task: ' + data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error archiving task:', error);
|
||||
alert('Failed to archive task: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async restoreTask(taskId) {
|
||||
if (confirm('Are you sure you want to restore this task? It will be moved back to completed status.')) {
|
||||
try {
|
||||
const response = await fetch(`/api/tasks/${taskId}/restore`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
await this.loadTasks();
|
||||
} else {
|
||||
alert('Failed to restore task: ' + data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error restoring task:', error);
|
||||
alert('Failed to restore task: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleTaskMove(evt) {
|
||||
|
||||
Reference in New Issue
Block a user