taskflow / backend /src /controllers /taskController.js
Sughan-0077's picture
🚀 Complete Taskflow Suite: Initial Clean Release
f6c14df
Raw
History Blame Contribute Delete
11.6 kB
const { query } = require('../utils/db');
const { auditLog } = require('../utils/auditLogger');
const { v4: uuidv4 } = require('uuid');
const VALID_STATUSES = ['todo', 'in_progress', 'in_review', 'done', 'cancelled'];
const VALID_PRIORITIES = ['low', 'medium', 'high', 'critical'];
/**
* GET /api/tasks
* List tasks scoped to the authenticated user's organization.
* Members only see their assigned or created tasks.
* Admins see all tasks in the org.
*/
const listTasks = async (req, res) => {
try {
const { organization_id: orgId, id: userId, role } = req.user;
const {
status, priority, assignee_id, search,
sort_by = 'created_at', sort_dir = 'desc',
page = 1, limit = 20,
} = req.query;
const pageNum = Math.max(1, parseInt(page));
const limitNum = Math.min(100, Math.max(1, parseInt(limit)));
const offset = (pageNum - 1) * limitNum;
const conditions = [`t.organization_id = $1`];
const params = [orgId];
let idx = 2;
// Members only see tasks they created or are assigned to
if (role === 'member') {
conditions.push(`(t.creator_id = $${idx} OR t.assignee_id = $${idx})`);
params.push(userId);
idx++;
}
if (status && VALID_STATUSES.includes(status)) {
conditions.push(`t.status = $${idx++}`);
params.push(status);
}
if (priority && VALID_PRIORITIES.includes(priority)) {
conditions.push(`t.priority = $${idx++}`);
params.push(priority);
}
if (assignee_id) {
conditions.push(`t.assignee_id = $${idx++}`);
params.push(assignee_id);
}
if (search) {
conditions.push(`(t.title ILIKE $${idx} OR t.description ILIKE $${idx})`);
params.push(`%${search}%`);
idx++;
}
const allowed_sort = ['created_at', 'updated_at', 'due_date', 'priority', 'status', 'title'];
const sortCol = allowed_sort.includes(sort_by) ? sort_by : 'created_at';
const sortDir = sort_dir === 'asc' ? 'ASC' : 'DESC';
const whereClause = `WHERE ${conditions.join(' AND ')}`;
const countResult = await query(
`SELECT COUNT(*) FROM tasks t ${whereClause}`,
params
);
const total = parseInt(countResult.rows[0].count);
const { rows: tasks } = await query(
`SELECT t.id, t.title, t.description, t.status, t.priority, t.due_date,
t.created_at, t.updated_at,
t.creator_id,
creator.name AS creator_name, creator.email AS creator_email,
t.assignee_id,
assignee.name AS assignee_name, assignee.email AS assignee_email
FROM tasks t
LEFT JOIN users creator ON creator.id = t.creator_id
LEFT JOIN users assignee ON assignee.id = t.assignee_id
${whereClause}
ORDER BY t.${sortCol} ${sortDir}
LIMIT $${idx} OFFSET $${idx + 1}`,
[...params, limitNum, offset]
);
return res.json({
tasks,
pagination: { page: pageNum, limit: limitNum, total, totalPages: Math.ceil(total / limitNum) },
});
} catch (err) {
console.error('List tasks error:', err);
return res.status(500).json({ error: 'Failed to retrieve tasks.' });
}
};
/**
* GET /api/tasks/:id
*/
const getTask = async (req, res) => {
try {
const { organization_id: orgId, id: userId, role } = req.user;
const { id } = req.params;
const { rows } = await query(
`SELECT t.id, t.title, t.description, t.status, t.priority, t.due_date,
t.created_at, t.updated_at, t.organization_id,
t.creator_id, creator.name AS creator_name, creator.email AS creator_email,
t.assignee_id, assignee.name AS assignee_name, assignee.email AS assignee_email
FROM tasks t
LEFT JOIN users creator ON creator.id = t.creator_id
LEFT JOIN users assignee ON assignee.id = t.assignee_id
WHERE t.id = $1 AND t.organization_id = $2`,
[id, orgId]
);
if (!rows.length) {
return res.status(404).json({ error: 'Task not found.' });
}
const task = rows[0];
// Members can only view tasks they created or are assigned to
if (role === 'member' && task.creator_id !== userId && task.assignee_id !== userId) {
return res.status(403).json({ error: 'You do not have permission to view this task.' });
}
return res.json(task);
} catch (err) {
console.error('Get task error:', err);
return res.status(500).json({ error: 'Failed to retrieve task.' });
}
};
/**
* POST /api/tasks
*/
const createTask = async (req, res) => {
try {
const { organization_id: orgId, id: userId, name: userName, email: userEmail } = req.user;
const { title, description, status = 'todo', priority = 'medium', assignee_id, due_date } = req.body;
// Validate assignee belongs to same org
if (assignee_id) {
const check = await query(`SELECT id FROM users WHERE id = $1 AND organization_id = $2`, [assignee_id, orgId]);
if (!check.rows.length) {
return res.status(400).json({ error: 'Assignee must belong to your organization.' });
}
}
const taskId = uuidv4();
const { rows } = await query(
`INSERT INTO tasks (id, organization_id, title, description, status, priority, creator_id, assignee_id, due_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *`,
[taskId, orgId, title, description || null, status, priority, userId, assignee_id || null, due_date || null]
);
const task = rows[0];
await auditLog({
organizationId: orgId,
taskId: task.id,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: 'TASK_CREATED',
newValues: { title, status, priority, assignee_id: assignee_id || null },
});
return res.status(201).json(task);
} catch (err) {
console.error('Create task error:', err);
return res.status(500).json({ error: 'Failed to create task.' });
}
};
/**
* PATCH /api/tasks/:id
*/
const updateTask = async (req, res) => {
try {
const { organization_id: orgId, id: userId, name: userName, email: userEmail, role } = req.user;
const { id } = req.params;
const { rows: existing } = await query(
`SELECT * FROM tasks WHERE id = $1 AND organization_id = $2`,
[id, orgId]
);
if (!existing.length) {
return res.status(404).json({ error: 'Task not found.' });
}
const task = existing[0];
// Members can only update tasks they created or are assigned to
if (role === 'member' && task.creator_id !== userId && task.assignee_id !== userId) {
return res.status(403).json({ error: 'You do not have permission to update this task.' });
}
const { title, description, status, priority, assignee_id, due_date } = req.body;
const updates = {};
if (title !== undefined) updates.title = title;
if (description !== undefined) updates.description = description;
if (status !== undefined && VALID_STATUSES.includes(status)) updates.status = status;
if (priority !== undefined && VALID_PRIORITIES.includes(priority)) updates.priority = priority;
if (due_date !== undefined) updates.due_date = due_date || null;
if (assignee_id !== undefined) {
if (assignee_id === null) {
updates.assignee_id = null;
} else {
const check = await query(`SELECT id FROM users WHERE id = $1 AND organization_id = $2`, [assignee_id, orgId]);
if (!check.rows.length) {
return res.status(400).json({ error: 'Assignee must belong to your organization.' });
}
updates.assignee_id = assignee_id;
}
}
if (!Object.keys(updates).length) {
return res.status(400).json({ error: 'No valid fields provided for update.' });
}
const setClauses = Object.keys(updates).map((k, i) => `${k} = $${i + 3}`);
const setValues = Object.values(updates);
const { rows: updated } = await query(
`UPDATE tasks SET ${setClauses.join(', ')}, updated_at = NOW()
WHERE id = $1 AND organization_id = $2
RETURNING *`,
[id, orgId, ...setValues]
);
await auditLog({
organizationId: orgId,
taskId: id,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: 'TASK_UPDATED',
oldValues: { title: task.title, status: task.status, priority: task.priority, assignee_id: task.assignee_id },
newValues: updates,
});
return res.json(updated[0]);
} catch (err) {
console.error('Update task error:', err);
return res.status(500).json({ error: 'Failed to update task.' });
}
};
/**
* DELETE /api/tasks/:id
*/
const deleteTask = async (req, res) => {
try {
const { organization_id: orgId, id: userId, name: userName, email: userEmail, role } = req.user;
const { id } = req.params;
const { rows: existing } = await query(
`SELECT * FROM tasks WHERE id = $1 AND organization_id = $2`,
[id, orgId]
);
if (!existing.length) {
return res.status(404).json({ error: 'Task not found.' });
}
const task = existing[0];
// Members can only delete tasks they created
if (role === 'member' && task.creator_id !== userId) {
return res.status(403).json({ error: 'You can only delete tasks you created.' });
}
await query(`DELETE FROM tasks WHERE id = $1 AND organization_id = $2`, [id, orgId]);
await auditLog({
organizationId: orgId,
taskId: id,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: 'TASK_DELETED',
oldValues: { title: task.title, status: task.status, priority: task.priority },
});
return res.status(204).send();
} catch (err) {
console.error('Delete task error:', err);
return res.status(500).json({ error: 'Failed to delete task.' });
}
};
/**
* GET /api/tasks/stats
* Dashboard stats for the org
*/
const getStats = async (req, res) => {
try {
const { organization_id: orgId, id: userId, role } = req.user;
let scopeClause = `organization_id = $1`;
const params = [orgId];
if (role === 'member') {
scopeClause += ` AND (creator_id = $2 OR assignee_id = $2)`;
params.push(userId);
}
const { rows: statusCounts } = await query(
`SELECT status, COUNT(*) as count FROM tasks WHERE ${scopeClause} GROUP BY status`,
params
);
const { rows: priorityCounts } = await query(
`SELECT priority, COUNT(*) as count FROM tasks WHERE ${scopeClause} GROUP BY priority`,
params
);
const { rows: overdue } = await query(
`SELECT COUNT(*) as count FROM tasks
WHERE ${scopeClause} AND due_date < NOW() AND status NOT IN ('done', 'cancelled')`,
params
);
const { rows: recent } = await query(
`SELECT t.id, t.title, t.status, t.priority, t.updated_at,
assignee.name AS assignee_name
FROM tasks t
LEFT JOIN users assignee ON assignee.id = t.assignee_id
WHERE t.${scopeClause}
ORDER BY t.updated_at DESC LIMIT 5`,
params
);
return res.json({
statusCounts: Object.fromEntries(statusCounts.map(r => [r.status, parseInt(r.count)])),
priorityCounts: Object.fromEntries(priorityCounts.map(r => [r.priority, parseInt(r.count)])),
overdueCount: parseInt(overdue[0].count),
recentTasks: recent,
});
} catch (err) {
console.error('Stats error:', err);
return res.status(500).json({ error: 'Failed to retrieve stats.' });
}
};
module.exports = { listTasks, getTask, createTask, updateTask, deleteTask, getStats };