Files
worklenz/worklenz-backend/src/controllers/tasks-controller.ts
chamikaJ 8825b0410a init
2025-04-17 18:28:54 +05:30

660 lines
25 KiB
TypeScript

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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
// 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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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<IWorkLenzResponse> {
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));
}
}