Add Task Archive feature.

This commit is contained in:
2025-07-06 08:49:09 +02:00
parent 8f63817194
commit 397175f38e
5 changed files with 489 additions and 4 deletions

88
app.py
View File

@@ -69,6 +69,15 @@ def run_migrations():
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
init_system_settings() 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!") print("PostgreSQL setup completed successfully!")
else: else:
print("Using SQLite - running SQLite migrations...") print("Using SQLite - running SQLite migrations...")
@@ -3996,6 +4005,13 @@ def update_task_status(task_id):
# Clear completion date if moving away from completed # Clear completion date if moving away from completed
task.completed_date = None 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() db.session.commit()
return jsonify({ return jsonify({
@@ -4199,6 +4215,78 @@ def remove_task_dependency(task_id, dependency_task_id):
return jsonify({'success': False, 'message': str(e)}) 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 # Sprint Management APIs
@app.route('/api/sprints') @app.route('/api/sprints')
@role_required(Role.TEAM_MEMBER) @role_required(Role.TEAM_MEMBER)

View File

@@ -75,6 +75,10 @@ def run_all_migrations(db_path=None):
migrate_system_events(db_path) migrate_system_events(db_path)
migrate_dashboard_system(db_path) migrate_dashboard_system(db_path)
# Run PostgreSQL-specific migrations if applicable
if FLASK_AVAILABLE:
migrate_postgresql_schema()
if FLASK_AVAILABLE: if FLASK_AVAILABLE:
with app.app_context(): with app.app_context():
# Handle company migration and admin user setup # Handle company migration and admin user setup
@@ -604,24 +608,61 @@ def migrate_task_system(db_path):
cursor.execute(""" cursor.execute("""
CREATE TABLE task ( CREATE TABLE task (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
task_number VARCHAR(20) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL, name VARCHAR(200) NOT NULL,
description TEXT, description TEXT,
status VARCHAR(50) DEFAULT 'Not Started', status VARCHAR(50) DEFAULT 'Not Started',
priority VARCHAR(50) DEFAULT 'Medium', priority VARCHAR(50) DEFAULT 'Medium',
estimated_hours FLOAT, estimated_hours FLOAT,
project_id INTEGER NOT NULL, project_id INTEGER NOT NULL,
sprint_id INTEGER,
assigned_to_id INTEGER, assigned_to_id INTEGER,
start_date DATE, start_date DATE,
due_date DATE, due_date DATE,
completed_date DATE, completed_date DATE,
archived_date DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by_id INTEGER NOT NULL, created_by_id INTEGER NOT NULL,
FOREIGN KEY (project_id) REFERENCES project (id), FOREIGN KEY (project_id) REFERENCES project (id),
FOREIGN KEY (sprint_id) REFERENCES sprint (id),
FOREIGN KEY (assigned_to_id) REFERENCES user (id), FOREIGN KEY (assigned_to_id) REFERENCES user (id),
FOREIGN KEY (created_by_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 # Check if sub_task table exists
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='sub_task'") 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 # Add category_id to project table if it doesn't exist
cursor.execute("PRAGMA table_info(project)") cursor.execute("PRAGMA table_info(project)")
project_columns = [column[1] for column in cursor.fetchall()] project_columns = [column[1] for column in cursor.fetchall()]
@@ -884,6 +968,120 @@ def create_all_tables(cursor):
print("All tables created") 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): def migrate_dashboard_system(db_file=None):
"""Migrate to add Dashboard widget system.""" """Migrate to add Dashboard widget system."""
db_path = get_db_path(db_file) db_path = get_db_path(db_file)
@@ -1045,6 +1243,8 @@ def main():
help='Run only system events migration') help='Run only system events migration')
parser.add_argument('--dashboard', '--dash', action='store_true', parser.add_argument('--dashboard', '--dash', action='store_true',
help='Run only dashboard system migration') help='Run only dashboard system migration')
parser.add_argument('--postgresql', '--pg', action='store_true',
help='Run only PostgreSQL schema migration')
args = parser.parse_args() args = parser.parse_args()
@@ -1081,6 +1281,9 @@ def main():
elif args.dashboard: elif args.dashboard:
migrate_dashboard_system(db_path) migrate_dashboard_system(db_path)
elif args.postgresql:
migrate_postgresql_schema()
else: else:
# Default: run all migrations # Default: run all migrations
run_all_migrations(db_path) run_all_migrations(db_path)

View File

@@ -421,6 +421,7 @@ class TaskStatus(enum.Enum):
IN_PROGRESS = "In Progress" IN_PROGRESS = "In Progress"
ON_HOLD = "On Hold" ON_HOLD = "On Hold"
COMPLETED = "Completed" COMPLETED = "Completed"
ARCHIVED = "Archived"
CANCELLED = "Cancelled" CANCELLED = "Cancelled"
# Task priority enumeration # Task priority enumeration
@@ -455,6 +456,7 @@ class Task(db.Model):
start_date = db.Column(db.Date, nullable=True) start_date = db.Column(db.Date, nullable=True)
due_date = db.Column(db.Date, nullable=True) due_date = db.Column(db.Date, nullable=True)
completed_date = db.Column(db.Date, nullable=True) completed_date = db.Column(db.Date, nullable=True)
archived_date = db.Column(db.Date, nullable=True)
# Metadata # Metadata
created_at = db.Column(db.DateTime, default=datetime.now) created_at = db.Column(db.DateTime, default=datetime.now)

View File

@@ -40,6 +40,7 @@
<option value="IN_PROGRESS">In Progress</option> <option value="IN_PROGRESS">In Progress</option>
<option value="ON_HOLD">On Hold</option> <option value="ON_HOLD">On Hold</option>
<option value="COMPLETED">Completed</option> <option value="COMPLETED">Completed</option>
<option value="ARCHIVED">Archived</option>
</select> </select>
</div> </div>
</div> </div>

View File

@@ -22,6 +22,7 @@
<div class="management-actions task-actions"> <div class="management-actions task-actions">
<button id="add-task-btn" class="btn btn-primary">+ Add Task</button> <button id="add-task-btn" class="btn btn-primary">+ Add Task</button>
<button id="refresh-tasks" class="btn btn-secondary">🔄 Refresh</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> </div>
</div> </div>
@@ -44,6 +45,10 @@
<div class="stat-number" id="overdue-tasks">0</div> <div class="stat-number" id="overdue-tasks">0</div>
<div class="stat-label">Overdue</div> <div class="stat-label">Overdue</div>
</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> </div>
<!-- Task Board --> <!-- Task Board -->
@@ -87,6 +92,16 @@
<!-- Task cards will be populated here --> <!-- Task cards will be populated here -->
</div> </div>
</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> </div>
<!-- Loading and Error States --> <!-- Loading and Error States -->
@@ -283,6 +298,88 @@
font-size: 0.8rem; 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 { .column-content {
min-height: 400px; min-height: 400px;
padding: 0.5rem 0; padding: 0.5rem 0;
@@ -879,6 +976,7 @@ class UnifiedTaskManager {
}; };
this.currentTask = null; this.currentTask = null;
this.sortableInstances = []; this.sortableInstances = [];
this.showArchived = false;
this.currentUserId = {{ g.user.id|tojson }}; this.currentUserId = {{ g.user.id|tojson }};
this.smartSearch = new SmartTaskSearch(); this.smartSearch = new SmartTaskSearch();
this.searchQuery = ''; this.searchQuery = '';
@@ -906,6 +1004,10 @@ class UnifiedTaskManager {
this.loadTasks(); this.loadTasks();
}); });
document.getElementById('toggle-archived').addEventListener('click', () => {
this.toggleArchivedView();
});
// Date validation // Date validation
document.getElementById('task-due-date').addEventListener('blur', () => { document.getElementById('task-due-date').addEventListener('blur', () => {
const input = document.getElementById('task-due-date'); const input = document.getElementById('task-due-date');
@@ -1287,7 +1389,8 @@ class UnifiedTaskManager {
'NOT_STARTED': [], 'NOT_STARTED': [],
'IN_PROGRESS': [], 'IN_PROGRESS': [],
'ON_HOLD': [], 'ON_HOLD': [],
'COMPLETED': [] 'COMPLETED': [],
'ARCHIVED': []
}; };
filteredTasks.forEach(task => { filteredTasks.forEach(task => {
@@ -1404,7 +1507,23 @@ class UnifiedTaskManager {
card.addEventListener('click', () => this.openTaskModal(task)); card.addEventListener('click', () => this.openTaskModal(task));
const dueDate = task.due_date ? new Date(task.due_date) : null; 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 = ` card.innerHTML = `
<div class="task-card-header"> <div class="task-card-header">
@@ -1419,6 +1538,9 @@ class UnifiedTaskManager {
<span class="task-assignee">${task.assigned_to_name || 'Unassigned'}</span> <span class="task-assignee">${task.assigned_to_name || 'Unassigned'}</span>
${dueDate ? `<span class="task-due-date ${isOverdue ? 'overdue' : ''}">${formatUserDate(task.due_date)}</span>` : ''} ${dueDate ? `<span class="task-due-date ${isOverdue ? 'overdue' : ''}">${formatUserDate(task.due_date)}</span>` : ''}
</div> </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; return card;
@@ -1432,15 +1554,84 @@ class UnifiedTaskManager {
const total = this.tasks.length; const total = this.tasks.length;
const completed = this.tasks.filter(t => t.status === 'COMPLETED').length; const completed = this.tasks.filter(t => t.status === 'COMPLETED').length;
const inProgress = this.tasks.filter(t => t.status === 'IN_PROGRESS').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 overdue = this.tasks.filter(t => {
const dueDate = t.due_date ? new Date(t.due_date) : null; 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; }).length;
document.getElementById('total-tasks').textContent = total; document.getElementById('total-tasks').textContent = total;
document.getElementById('completed-tasks').textContent = completed; document.getElementById('completed-tasks').textContent = completed;
document.getElementById('in-progress-tasks').textContent = inProgress; document.getElementById('in-progress-tasks').textContent = inProgress;
document.getElementById('overdue-tasks').textContent = overdue; 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) { async handleTaskMove(evt) {