203 lines
7.8 KiB
JavaScript
203 lines
7.8 KiB
JavaScript
import { Router } from 'express';
|
|
import pool from '../db.js';
|
|
import { randomUUID } from 'crypto';
|
|
|
|
const router = Router();
|
|
|
|
// Helper: fetch full task with subtasks, comments, activities, tags
|
|
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]);
|
|
|
|
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() || '' })),
|
|
};
|
|
}
|
|
|
|
// 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 } = 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]);
|
|
}
|
|
}
|
|
|
|
// Add creation activity
|
|
const actId = randomUUID();
|
|
await conn.query('INSERT INTO activities (id, task_id, text) VALUES (?, ?, ?)',
|
|
[actId, id, '📝 Task created']);
|
|
|
|
await conn.commit();
|
|
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]);
|
|
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' });
|
|
}
|
|
});
|
|
|
|
// 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;
|