import moment from "moment"; import { IWorkLenzRequest } from "../interfaces/worklenz-request"; import { IWorkLenzResponse } from "../interfaces/worklenz-response"; import db from "../config/db"; import { ServerResponse } from "../models/server-response"; import { S3_URL, TASK_STATUS_COLOR_ALPHA } from "../shared/constants"; import { getDates, getMinMaxOfTaskDates, getMonthRange, getWeekRange } from "../shared/tasks-controller-utils"; import { getColor, getRandomColorCode, humanFileSize, log_error, toMinutes } from "../shared/utils"; import WorklenzControllerBase from "./worklenz-controller-base"; import HandleExceptions from "../decorators/handle-exceptions"; import { NotificationsService } from "../services/notifications/notifications.service"; import { getTaskCompleteInfo } from "../socket.io/commands/on-quick-task"; import { getAssignees, getTeamMembers } from "../socket.io/commands/on-quick-assign-or-remove"; import TasksControllerV2 from "./tasks-controller-v2"; import { IO } from "../shared/io"; import { SocketEvents } from "../socket.io/events"; import TasksControllerBase from "./tasks-controller-base"; import { insertToActivityLogs } from "../services/activity-logs/activity-logs.service"; import { IActivityLog } from "../services/activity-logs/interfaces"; import { getKey, getRootDir, uploadBase64 } from "../shared/s3"; export default class TasksController extends TasksControllerBase { private static notifyProjectUpdates(socketId: string, projectId: string) { IO.getSocketById(socketId) ?.to(projectId) .emit(SocketEvents.PROJECT_UPDATES_AVAILABLE.toString()); } public static async uploadAttachment(attachments: any, teamId: string, userId: string) { try { const promises = attachments.map(async (attachment: any) => { const { file, file_name, project_id, size } = attachment; const type = file_name.split(".").pop(); const q = ` INSERT INTO task_attachments (name, task_id, team_id, project_id, uploaded_by, size, type) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, name, size, type, created_at, CONCAT($8::TEXT, '/', team_id, '/', project_id, '/', id, '.', type) AS url; `; const result = await db.query(q, [ file_name, null, teamId, project_id, userId, size, type, `${S3_URL}/${getRootDir()}` ]); const [data] = result.rows; await uploadBase64(file, getKey(teamId, project_id, data.id, data.type)); return data.id; }); const attachmentIds = await Promise.all(promises); return attachmentIds; } catch (error) { log_error(error); } } @HandleExceptions() public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const userId = req.user?.id as string; const teamId = req.user?.team_id as string; if (req.body.attachments_raw) { req.body.attachments = await this.uploadAttachment(req.body.attachments_raw, teamId, userId); } const q = `SELECT create_task($1) AS task;`; const result = await db.query(q, [JSON.stringify(req.body)]); const [data] = result.rows; for (const member of data?.task.assignees || []) { NotificationsService.createTaskUpdate( "ASSIGN", userId, data.task.id, member.user_id, member.team_id ); } return res.status(200).send(new ServerResponse(true, data.task)); } @HandleExceptions() public static async getGanttTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = `SELECT get_gantt_tasks($1) AS gantt_tasks;`; const result = await db.query(q, [req.user?.id ?? null]); const [data] = result.rows; return res.status(200).send(new ServerResponse(true, data.gantt_tasks)); } private static sendAssignmentNotifications(task: any, userId: string) { const newMembers = task.new_assignees.filter((member1: any) => { return !task.old_assignees.some((member2: any) => { return member1.team_member_id === member2.team_member_id; }); }); const removedMembers = task.old_assignees.filter((member1: any) => { return !task.new_assignees.some((member2: any) => { return member1.team_member_id === member2.team_member_id; }); }); for (const member of newMembers) { NotificationsService.createTaskUpdate( "ASSIGN", userId, task.id, member.user_id, member.team_id ); } for (const member of removedMembers) { NotificationsService.createTaskUpdate( "UNASSIGN", userId, task.id, member.user_id, member.team_id ); } } public static async notifyStatusChange(userId: string, taskId: string, statusId: string) { try { const q2 = "SELECT handle_on_task_status_change($1, $2, $3) AS res;"; const results1 = await db.query(q2, [userId, taskId, statusId]); const [d] = results1.rows; const changeResponse = d.res; // notify to all task members of the change for (const member of changeResponse.members || []) { if (member.user_id === userId) continue; NotificationsService.createNotification({ userId: member.user_id, teamId: member.team_id, socketId: member.socket_id, message: changeResponse.message, taskId, projectId: changeResponse.project_id }); } } catch (error) { log_error(error); } } @HandleExceptions() public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const userId = req.user?.id as string; await this.notifyStatusChange(userId, req.body.id, req.body.status_id); const q = `SELECT update_task($1) AS task;`; const result = await db.query(q, [JSON.stringify(req.body)]); const [data] = result.rows; const task = data.task || null; if (task) { this.sendAssignmentNotifications(task, userId); } return res.status(200).send(new ServerResponse(true, result.rows)); } @HandleExceptions() public static async updateDuration(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const { id } = req.params; const { start, end } = req.body; const q = ` UPDATE tasks SET start_date = ($1)::TIMESTAMP, end_date = ($2)::TIMESTAMP WHERE id = ($3)::UUID RETURNING id; `; const result = await db.query(q, [start, end, id]); const [data] = result.rows; if (data?.id) return res.status(200).send(new ServerResponse(true, {})); return res.status(200).send(new ServerResponse(false, {}, "Task update failed!")); } @HandleExceptions() public static async updateStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const { status_id, task_id } = req.params; const { project_id, from_index, to_index } = req.body; const q = `SELECT update_task_status($1, $2, $3, $4, $5) AS status;`; const result = await db.query(q, [task_id, project_id, status_id, from_index, to_index]); const [data] = result.rows; if (data?.status) return res.status(200).send(new ServerResponse(true, {})); return res.status(200).send(new ServerResponse(false, {}, "Task update failed!")); } @HandleExceptions() public static async getTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const { id } = req.params; const q = `SELECT get_project_gantt_tasks($1) AS gantt_tasks;`; const result = await db.query(q, [id]); const [data] = result.rows; return res.status(200).send(new ServerResponse(true, data?.gantt_tasks)); } @HandleExceptions() public static async getTasksBetweenRange(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const { project_id, start_date, end_date } = req.query; const q = ` SELECT pm.id, (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]' ::JSON) FROM (SELECT t.id, t.name, t.start_date, t.project_id, t.priority_id, t.done, t.end_date, (SELECT color_code FROM projects WHERE projects.id = t.project_id) AS color_code, (SELECT name FROM task_statuses WHERE id = t.status_id) AS status FROM tasks_assignees ta, tasks t WHERE t.archived IS FALSE AND ta.project_member_id = pm.id AND t.id = ta.task_id AND start_date IS NOT NULL AND end_date IS NOT NULL ORDER BY start_date) rec) AS tasks FROM project_members pm WHERE project_id = $1; `; const result = await db.query(q, [project_id]); const obj: any = {}; const minMaxDates: { min_date: string, max_date: string } = await getMinMaxOfTaskDates(project_id as string); const dates = await getDates(minMaxDates.min_date || start_date as string, minMaxDates.max_date || end_date as string); const months = await getWeekRange(dates); for (const element of result.rows) { obj[element.id] = element.tasks; for (const task of element.tasks) { const min: number = dates.findIndex((date) => moment(task.start_date).isSame(date.date, "days")); const max: number = dates.findIndex((date) => moment(task.end_date).isSame(date.date, "days")); task.min = min + 1; task.max = max > 0 ? max + 2 : max; } } return res.status(200).send(new ServerResponse(true, { tasks: [obj], dates, months })); } @HandleExceptions() public static async getGanttTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = ` SELECT id, name, start_date, project_id, priority_id, done, end_date, (SELECT color_code FROM projects WHERE projects.id = project_id) AS color_code, (SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status, parent_task_id, parent_task_id IS NOT NULL AS is_sub_task, (SELECT name FROM tasks WHERE id = tasks.parent_task_id) AS parent_task_name, (SELECT COUNT('*')::INT FROM tasks WHERE parent_task_id = tasks.id) AS sub_tasks_count FROM tasks WHERE archived IS FALSE AND project_id = $1 AND parent_task_id IS NULL ORDER BY start_date; `; const result = await db.query(q, [req.query.project_id]); const minMaxDates: { min_date: string, max_date: string } = await getMinMaxOfTaskDates(req.query.project_id as string); if (!minMaxDates.max_date && !minMaxDates.min_date) { minMaxDates.min_date = moment().format(); minMaxDates.max_date = moment().add(45, "days").format(); } const dates = await getDates(minMaxDates.min_date, minMaxDates.max_date); const weeks = await getWeekRange(dates); const months = await getMonthRange(dates); for (const task of result.rows) { const min: number = dates.findIndex((date) => moment(task.start_date).isSame(date.date, "days")); const max: number = dates.findIndex((date) => moment(task.end_date).isSame(date.date, "days")); task.show_sub_tasks = false; task.sub_tasks = []; task.min = min + 1; task.max = max > 0 ? max + 2 : max; } return res.status(200).send(new ServerResponse(true, { tasks: result.rows, dates, weeks, months })); } @HandleExceptions() public static async getProjectTasksByTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = `SELECT get_resource_gantt_tasks($1) AS gantt_tasks;`; const result = await db.query(q, [req.user?.id ?? null]); const [data] = result.rows; return res.status(200).send(new ServerResponse(true, data?.gantt_tasks)); } @HandleExceptions() public static async getSelectedTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = `SELECT get_selected_tasks($1) AS tasks`; const result = await db.query(q, [req.params.id]); const [data] = result.rows; return res.status(200).send(new ServerResponse(true, data?.tasks)); } @HandleExceptions() public static async getUnselectedTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = `SELECT get_unselected_tasks($1) AS tasks`; const result = await db.query(q, [req.params.id]); const [data] = result.rows; return res.status(200).send(new ServerResponse(true, data?.tasks)); } /** Should migrate getProjectTasksByStatus to this */ @HandleExceptions() public static async getProjectTasksByStatusV2(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { // Get all statuses const q1 = ` SELECT task_statuses.id, task_statuses.name, stsc.color_code FROM task_statuses INNER JOIN sys_task_status_categories stsc ON task_statuses.category_id = stsc.id WHERE project_id = $1 AND team_id = $2 ORDER BY task_statuses.sort_order; `; const result1 = await db.query(q1, [req.query.project, req.user?.team_id]); const statuses = result1.rows; const dataset = []; // Query tasks of statuses for (const status of statuses) { const q2 = `SELECT get_tasks_by_status($1, $2) AS tasks`; const result2 = await db.query(q2, [req.params.id, status]); const [data] = result2.rows; for (const task of data.tasks) { task.name_color = getColor(task.name); task.names = this.createTagList(task.assignees); task.names.map((a: any) => a.color_code = getColor(a.name)); } dataset.push(data); } return res.status(200).send(new ServerResponse(true, dataset)); } @HandleExceptions() public static async getProjectTasksByStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = `SELECT get_tasks_by_status($1,$2) AS tasks`; const result = await db.query(q, [req.params.id, req.query.status]); const [data] = result.rows; for (const task of data.tasks) { task.name_color = getColor(task.name); task.names = this.createTagList(task.assignees); task.all_labels = task.labels; task.labels = this.createTagList(task.labels, 3); task.names.map((a: any) => a.color_code = getColor(a.name)); } return res.status(200).send(new ServerResponse(true, data?.tasks)); } @HandleExceptions() public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = `DELETE FROM tasks WHERE id = $1;`; const result = await db.query(q, [req.params.id]); return res.status(200).send(new ServerResponse(true, result.rows)); } @HandleExceptions() public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = `SELECT get_task_form_view_model($1, $2, $3, $4) AS view_model;`; const result = await db.query(q, [req.user?.id ?? null, req.user?.team_id ?? null, req.query.task_id ?? null, (req.query.project_id as string) || null]); const [data] = result.rows; const default_model = { task: {}, priorities: [], projects: [], statuses: [], team_members: [], }; const task = data.view_model.task || null; if (!task) return res.status(200).send(new ServerResponse(true, default_model)); if (data.view_model && task) { task.assignees.map((a: any) => { a.color_code = getColor(a.name); return a; }); task.names = WorklenzControllerBase.createTagList(task.assignees); const totalMinutes = task.total_minutes; const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; task.total_hours = hours; task.total_minutes = minutes; task.assignees = (task.assignees || []).map((i: any) => i.team_member_id); task.timer_start_time = moment(task.timer_start_time).valueOf(); task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA; } for (const member of (data.view_model?.team_members || [])) { member.color_code = getColor(member.name); } const t = await getTaskCompleteInfo(task); const info = await TasksControllerV2.getTaskCompleteRatio(t.parent_task_id || t.id); if (info) { t.complete_ratio = info.ratio; t.completed_count = info.total_completed; t.total_tasks_count = info.total_tasks; } data.view_model.task = t; return res.status(200).send(new ServerResponse(true, data.view_model || default_model)); } @HandleExceptions() public static async createQuickTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = `SELECT create_quick_task($1) AS task_id;`; req.body.reporter_id = req.user?.id ?? null; req.body.team_id = req.user?.team_id ?? null; req.body.total_minutes = toMinutes(req.body.total_hours, req.body.total_minutes); const result = await db.query(q, [JSON.stringify(req.body)]); const [data] = result.rows; return res.status(200).send(new ServerResponse(true, data)); } @HandleExceptions() public static async createHomeTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = `SELECT create_home_task($1);`; let endDate = req.body.end_date; switch (endDate) { case "Today": endDate = moment().format(); break; case "Tomorrow": endDate = moment().add(1, "days").format(); break; case "Next Week": endDate = moment().add(1, "weeks").endOf("isoWeek").format(); break; case "Next Month": endDate = moment().add(1, "months").endOf("month").format(); break; case "No Due Date": endDate = null; break; default: endDate = null; } req.body.end_date = endDate; req.body.reporter_id = req.user?.id ?? null; req.body.team_id = req.user?.team_id ?? null; const result = await db.query(q, [JSON.stringify(req.body)]); const [data] = result.rows; return res.status(200).send(new ServerResponse(true, data.create_home_task.task)); } @HandleExceptions() public static async bulkChangeStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = `SELECT bulk_change_tasks_status($1, $2) AS task;`; const result = await db.query(q, [JSON.stringify(req.body), req.user?.id]); const [data] = result.rows; TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string); return res.status(200).send(new ServerResponse(true, { failed_tasks: data.task })); } @HandleExceptions() public static async bulkChangePriority(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = `SELECT bulk_change_tasks_priority($1, $2) AS task;`; const result = await db.query(q, [JSON.stringify(req.body), req.user?.id]); const [data] = result.rows; TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string); return res.status(200).send(new ServerResponse(true, data)); } @HandleExceptions() public static async bulkChangePhase(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = `SELECT bulk_change_tasks_phase($1, $2) AS task;`; const result = await db.query(q, [JSON.stringify(req.body), req.user?.id]); const [data] = result.rows; TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string); return res.status(200).send(new ServerResponse(true, data)); } @HandleExceptions() public static async bulkDelete(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const deletedTasks = req.body.tasks.map((t: any) => t.id); const result: any = { deleted_tasks: deletedTasks }; const q = `SELECT bulk_delete_tasks($1) AS task;`; await db.query(q, [JSON.stringify(req.body)]); TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string); return res.status(200).send(new ServerResponse(true, result)); } @HandleExceptions() public static async bulkArchive(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = `SELECT bulk_archive_tasks($1) AS task;`; req.body.type = req.query.type; await db.query(q, [JSON.stringify(req.body)]); const tasks = req.body.tasks.map((t: any) => t.id); TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string); return res.status(200).send(new ServerResponse(true, tasks)); } @HandleExceptions() public static async bulkAssignMe(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { req.body.team_id = req.user?.team_id; req.body.user_id = req.user?.id; const [task] = req.body.tasks || []; const q = `SELECT bulk_assign_to_me($1) AS task;`; await db.query(q, [JSON.stringify(req.body)]); const assignees = await getAssignees(task.id); const members = await getTeamMembers(req.body.team_id); // for inline display const names = WorklenzControllerBase.createTagList(assignees); const data = { id: task.id, members, assignees, names }; const activityLog: IActivityLog = { task_id: task.id, attribute_type: "assignee", user_id: req.user?.id, log_type: "assign", old_value: null, new_value: req.user?.id, next_string: req.user?.name }; insertToActivityLogs(activityLog); TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string); return res.status(200).send(new ServerResponse(true, data)); } @HandleExceptions() public static async bulkAssignLabel(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { if (req.body.text) { const q0 = `SELECT bulk_assign_or_create_label($1) AS label;`; req.body.team_id = req.user?.team_id; req.body.color = getRandomColorCode(); await db.query(q0, [JSON.stringify(req.body)]); } else { const q = `SELECT bulk_assign_label($1, $2) AS task;`; await db.query(q, [JSON.stringify(req.body), req.user?.id as string]); } TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string); return res.status(200).send(new ServerResponse(true, null)); } @HandleExceptions() public static async bulkAssignMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const { tasks, members, project_id } = req.body; try { for (const task of tasks) { for (const member of members) { await TasksController.createTaskBulkAssignees(member.id, project_id, task.id, req.user?.id as string); } } TasksController.notifyProjectUpdates(req.user?.socket_id as string, project_id as string); return res.status(200).send(new ServerResponse(true, null)); } catch (error) { return res.status(500).send(new ServerResponse(false, "An error occurred")); } } public static async createTaskAssignee(memberId: string, projectId: string, taskId: string, userId: string) { const q = `SELECT create_task_assignee($1,$2,$3,$4)`; const result = await db.query(q, [memberId, projectId, taskId, userId]); return result.rows; } public static async createTaskBulkAssignees(memberId: string, projectId: string, taskId: string, userId: string) { const q = `SELECT create_bulk_task_assignees($1,$2,$3,$4)`; const result = await db.query(q, [memberId, projectId, taskId, userId]); return result.rows; } @HandleExceptions() public static async getProjectTaskAssignees(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const q = ` SELECT project_members.team_member_id AS id, tmiv.name, tmiv.email, tmiv.avatar_url FROM project_members LEFT JOIN team_member_info_view tmiv ON project_members.team_member_id = tmiv.team_member_id WHERE project_id = $1 AND EXISTS(SELECT 1 FROM tasks_assignees WHERE project_member_id = project_members.id); `; const result = await db.query(q, [req.params.id]); for (const member of result.rows) { member.color_code = getColor(member.name); } return res.status(200).send(new ServerResponse(true, result.rows)); } }