import { Router } from 'express'; import pool from '../db.js'; import { randomUUID } from 'crypto'; import { createNotification } from './notifications.js'; const router = Router(); // Helper: fetch full task with subtasks, comments, activities, tags, dependencies async function getFullTask(taskId) { const [taskRows] = await pool.query('SELECT * FROM tasks WHERE id = ?', [taskId]); if (taskRows.length === 0) return null; const task = taskRows[0]; const [subtasks] = await pool.query('SELECT id, title, done FROM subtasks WHERE task_id = ? ORDER BY id', [taskId]); const [comments] = await pool.query('SELECT id, user_id AS userId, text, timestamp FROM comments WHERE task_id = ? ORDER BY timestamp', [taskId]); const [activities] = await pool.query('SELECT id, text, timestamp FROM activities WHERE task_id = ? ORDER BY timestamp', [taskId]); const [tagRows] = await pool.query('SELECT tag FROM task_tags WHERE task_id = ?', [taskId]); const [depRows] = await pool.query('SELECT id, depends_on_user_id AS dependsOnUserId, description, resolved FROM dependencies WHERE task_id = ? ORDER BY created_at', [taskId]); return { id: task.id, title: task.title, description: task.description || '', status: task.status, priority: task.priority, assignee: task.assignee_id || '', reporter: task.reporter_id || '', dueDate: task.due_date ? task.due_date.toISOString().split('T')[0] : '', tags: tagRows.map(r => r.tag), subtasks: subtasks.map(s => ({ id: s.id, title: s.title, done: !!s.done })), comments: comments.map(c => ({ id: c.id, userId: c.userId, text: c.text, timestamp: c.timestamp?.toISOString() || '' })), activity: activities.map(a => ({ id: a.id, text: a.text, timestamp: a.timestamp?.toISOString() || '' })), dependencies: depRows.map(d => ({ id: d.id, dependsOnUserId: d.dependsOnUserId || '', description: d.description, resolved: !!d.resolved })), }; } // GET /api/tasks router.get('/', async (_req, res) => { try { const [taskRows] = await pool.query('SELECT id FROM tasks ORDER BY created_at DESC'); const tasks = await Promise.all(taskRows.map(t => getFullTask(t.id))); res.json(tasks.filter(Boolean)); } catch (err) { console.error('Get tasks error:', err); res.status(500).json({ error: 'Internal server error' }); } }); // POST /api/tasks router.post('/', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); const { title, description, status, priority, assignee, reporter, dueDate, tags, subtasks, dependencies } = req.body; const id = randomUUID(); await conn.query( 'INSERT INTO tasks (id, title, description, status, priority, assignee_id, reporter_id, due_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [id, title, description || '', status || 'todo', priority || 'medium', assignee || null, reporter || null, dueDate || null] ); // Insert tags if (tags && tags.length > 0) { const tagValues = tags.map(tag => [id, tag]); await conn.query('INSERT INTO task_tags (task_id, tag) VALUES ?', [tagValues]); } // Insert subtasks if (subtasks && subtasks.length > 0) { for (const st of subtasks) { await conn.query('INSERT INTO subtasks (id, task_id, title, done) VALUES (?, ?, ?, ?)', [st.id || randomUUID(), id, st.title, st.done || false]); } } // Insert dependencies if (dependencies && dependencies.length > 0) { for (const dep of dependencies) { await conn.query( 'INSERT INTO dependencies (id, task_id, depends_on_user_id, description, resolved) VALUES (?, ?, ?, ?, ?)', [randomUUID(), id, dep.dependsOnUserId || null, dep.description, false] ); } } // Add creation activity const actId = randomUUID(); await conn.query('INSERT INTO activities (id, task_id, text) VALUES (?, ?, ?)', [actId, id, '📝 Task created']); await conn.commit(); if (assignee) { await createNotification(req, { userId: assignee, type: 'assignment', title: 'New Task Assigned', message: `You have been assigned to task: ${title}`, link: `/tasks?id=${id}` }); } const task = await getFullTask(id); res.status(201).json(task); } catch (err) { await conn.rollback(); console.error('Create task error:', err); res.status(500).json({ error: 'Internal server error' }); } finally { conn.release(); } }); // PUT /api/tasks/:id router.put('/:id', async (req, res) => { const conn = await pool.getConnection(); try { await conn.beginTransaction(); const { title, description, status, priority, assignee, reporter, dueDate, tags } = req.body; const taskId = req.params.id; // Check task exists const [existing] = await conn.query('SELECT id FROM tasks WHERE id = ?', [taskId]); if (existing.length === 0) { await conn.rollback(); return res.status(404).json({ error: 'Task not found' }); } await conn.query( `UPDATE tasks SET title = COALESCE(?, title), description = COALESCE(?, description), status = COALESCE(?, status), priority = COALESCE(?, priority), assignee_id = COALESCE(?, assignee_id), reporter_id = COALESCE(?, reporter_id), due_date = COALESCE(?, due_date) WHERE id = ?`, [title, description, status, priority, assignee, reporter, dueDate, taskId] ); // Update tags if provided if (tags !== undefined) { await conn.query('DELETE FROM task_tags WHERE task_id = ?', [taskId]); if (tags.length > 0) { const tagValues = tags.map(tag => [taskId, tag]); await conn.query('INSERT INTO task_tags (task_id, tag) VALUES ?', [tagValues]); } } await conn.commit(); const task = await getFullTask(taskId); res.json(task); } catch (err) { await conn.rollback(); console.error('Update task error:', err); res.status(500).json({ error: 'Internal server error' }); } finally { conn.release(); } }); // POST /api/tasks/:id/subtasks router.post('/:id/subtasks', async (req, res) => { try { const { title } = req.body; const id = randomUUID(); await pool.query('INSERT INTO subtasks (id, task_id, title, done) VALUES (?, ?, ?, ?)', [id, req.params.id, title, false]); res.status(201).json({ id, title, done: false }); } catch (err) { console.error('Add subtask error:', err); res.status(500).json({ error: 'Internal server error' }); } }); // PUT /api/tasks/:id/subtasks/:sid router.put('/:id/subtasks/:sid', async (req, res) => { try { const { done } = req.body; await pool.query('UPDATE subtasks SET done = ? WHERE id = ? AND task_id = ?', [done, req.params.sid, req.params.id]); res.json({ success: true }); } catch (err) { console.error('Toggle subtask error:', err); res.status(500).json({ error: 'Internal server error' }); } }); // POST /api/tasks/:id/comments router.post('/:id/comments', async (req, res) => { try { const { userId, text } = req.body; const id = randomUUID(); const timestamp = new Date(); await pool.query('INSERT INTO comments (id, task_id, user_id, text, timestamp) VALUES (?, ?, ?, ?, ?)', [id, req.params.id, userId, text, timestamp]); // Mention detection: @[Name](userId) const mentions = text.match(/@\[([^\]]+)\]\(([^)]+)\)/g); if (mentions) { const mentionedUserIds = [...new Set(mentions.map(m => m.match(/\(([^)]+)\)/)[1]))]; for (const mId of mentionedUserIds) { if (mId !== userId) { await createNotification(req, { userId: mId, type: 'mention', title: 'New Mention', message: `You were mentioned in a comment on task ${req.params.id}`, link: `/tasks?id=${req.params.id}` }); } } } res.status(201).json({ id, userId, text, timestamp: timestamp.toISOString() }); } catch (err) { console.error('Add comment error:', err); res.status(500).json({ error: 'Internal server error' }); } }); // POST /api/tasks/:id/activity router.post('/:id/activity', async (req, res) => { try { const { text } = req.body; const id = randomUUID(); const timestamp = new Date(); await pool.query('INSERT INTO activities (id, task_id, text, timestamp) VALUES (?, ?, ?, ?)', [id, req.params.id, text, timestamp]); res.status(201).json({ id, text, timestamp: timestamp.toISOString() }); } catch (err) { console.error('Add activity error:', err); res.status(500).json({ error: 'Internal server error' }); } }); // --- DEPENDENCY ROUTES --- // POST /api/tasks/:id/dependencies router.post('/:id/dependencies', async (req, res) => { try { const { dependsOnUserId, description } = req.body; const id = randomUUID(); await pool.query( 'INSERT INTO dependencies (id, task_id, depends_on_user_id, description, resolved) VALUES (?, ?, ?, ?, ?)', [id, req.params.id, dependsOnUserId || null, description, false] ); res.status(201).json({ id, dependsOnUserId: dependsOnUserId || '', description, resolved: false }); } catch (err) { console.error('Add dependency error:', err); res.status(500).json({ error: 'Internal server error' }); } }); // PUT /api/tasks/:id/dependencies/:depId router.put('/:id/dependencies/:depId', async (req, res) => { try { const { resolved } = req.body; await pool.query('UPDATE dependencies SET resolved = ? WHERE id = ? AND task_id = ?', [resolved, req.params.depId, req.params.id]); res.json({ success: true }); } catch (err) { console.error('Update dependency error:', err); res.status(500).json({ error: 'Internal server error' }); } }); // DELETE /api/tasks/:id/dependencies/:depId router.delete('/:id/dependencies/:depId', async (req, res) => { try { await pool.query('DELETE FROM dependencies WHERE id = ? AND task_id = ?', [req.params.depId, req.params.id]); res.json({ success: true }); } catch (err) { console.error('Delete dependency error:', err); res.status(500).json({ error: 'Internal server error' }); } }); // DELETE /api/tasks/:id router.delete('/:id', async (req, res) => { try { await pool.query('DELETE FROM tasks WHERE id = ?', [req.params.id]); res.json({ success: true }); } catch (err) { console.error('Delete task error:', err); res.status(500).json({ error: 'Internal server error' }); } }); export default router;