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

1096 lines
51 KiB
TypeScript

import moment from "moment";
import Excel from "exceljs";
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
import db from "../config/db";
import { IPassportSession } from "../interfaces/passport-session";
import { ServerResponse } from "../models/server-response";
import { sendInvitationEmail } from "../shared/email-templates";
import { IO } from "../shared/io";
import { SocketEvents } from "../socket.io/events";
import WorklenzControllerBase from "./worklenz-controller-base";
import HandleExceptions from "../decorators/handle-exceptions";
import { formatDuration, getColor } from "../shared/utils";
import { statusExclude, TEAM_MEMBER_TREE_MAP_COLOR_ALPHA } from "../shared/constants";
import { checkTeamSubscriptionStatus } from "../shared/paddle-utils";
import { updateUsers } from "../shared/paddle-requests";
import { NotificationsService } from "../services/notifications/notifications.service";
export default class TeamMembersController extends WorklenzControllerBase {
public static async checkIfUserAlreadyExists(owner_id: string, email: string) {
if (!owner_id) throw new Error("Owner not found.");
const q = `SELECT EXISTS(SELECT tmi.team_member_id
FROM team_member_info_view AS tmi
JOIN teams AS t ON tmi.team_id = t.id
WHERE tmi.email = $1::TEXT
AND t.user_id = $2::UUID);`;
const result = await db.query(q, [email, owner_id]);
const [data] = result.rows;
return data.exists;
}
public static async checkIfUserActiveInOtherTeams(owner_id: string, email: string) {
if (!owner_id) throw new Error("Owner not found.");
const q = `SELECT EXISTS(SELECT tmi.team_member_id
FROM team_member_info_view AS tmi
JOIN teams AS t ON tmi.team_id = t.id
JOIN team_members AS tm ON tmi.team_member_id = tm.id
WHERE tmi.email = $1::TEXT
AND t.user_id = $2::UUID AND tm.active = true);`;
const result = await db.query(q, [email, owner_id]);
const [data] = result.rows;
return data.exists;
}
public static async createOrInviteMembers<T>(body: T, user: IPassportSession): Promise<Array<{
name?: string;
email?: string;
is_new?: string;
team_member_id?: string;
team_member_user_id?: string;
}>> {
const q = `SELECT create_team_member($1) AS new_members;`;
const result = await db.query(q, [JSON.stringify(body)]);
const [data] = result.rows;
const newMembers = data?.new_members || [];
const projectId = (body as any)?.project_id;
NotificationsService.sendTeamMembersInvitations(newMembers, user, projectId || "");
return newMembers;
}
@HandleExceptions({
raisedExceptions: {
"ERROR_EMAIL_INVITATION_EXISTS": `Team member with email "{0}" already exists.`
}
})
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
req.body.team_id = req.user?.team_id || null;
if (!req.user?.team_id) {
return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
}
/**
* Checks the subscription status of the team.
* @type {Object} subscriptionData - Object containing subscription information
*/
const subscriptionData = await checkTeamSubscriptionStatus(req.user?.team_id);
let incrementBy = 0;
// Handle self-hosted subscriptions differently
if (subscriptionData.subscription_type === 'SELF_HOSTED') {
// Check if users exist and add them if they don't
await Promise.all(req.body.emails.map(async (email: string) => {
const trimmedEmail = email.trim();
const userExists = await this.checkIfUserAlreadyExists(req.user?.owner_id as string, trimmedEmail);
if (!userExists) {
incrementBy = incrementBy + 1;
}
}));
// Create or invite new members
const newMembers = await this.createOrInviteMembers(req.body, req.user);
return res.status(200).send(new ServerResponse(true, newMembers, `Your teammates will get an email that gives them access to your team.`).withTitle("Invitations sent"));
}
/**
* Iterates through each email in the request body and checks if the user already exists.
* If the user doesn't exist, increments the counter.
* @param {string} email - Email address to check
*/
await Promise.all(req.body.emails.map(async (email: string) => {
const trimmedEmail = email.trim();
const userExists = await this.checkIfUserAlreadyExists(req.user?.owner_id as string, trimmedEmail);
const isUserActive = await this.checkIfUserActiveInOtherTeams(req.user?.owner_id as string, trimmedEmail);
if (!userExists || !isUserActive) {
incrementBy = incrementBy + 1;
}
}));
/**
* Checks various conditions to determine if the maximum number of lifetime users is exceeded.
* Sends a response if the limit is reached.
*/
if (
incrementBy > 0
&& subscriptionData.is_ltd
&& subscriptionData.current_count
&& ((parseInt(subscriptionData.current_count) + req.body.emails.length) > parseInt(subscriptionData.ltd_users))) {
return res.status(200).send(new ServerResponse(false, null, "Cannot exceed the maximum number of life time users."));
}
if (
subscriptionData.is_ltd
&& subscriptionData.current_count
&& ((parseInt(subscriptionData.current_count) + incrementBy) > parseInt(subscriptionData.ltd_users))) {
return res.status(200).send(new ServerResponse(false, null, "Cannot exceed the maximum number of life time users."));
}
/**
* Checks subscription details and updates the user count if applicable.
* Sends a response if there is an issue with the subscription.
*/
// if (!subscriptionData.is_credit && !subscriptionData.is_custom && subscriptionData.subscription_status === "active") {
// const response = await updateUsers(subscriptionData.subscription_id, (subscriptionData.quantity + incrementBy));
// if (!response.body.subscription_id) {
// return res.status(200).send(new ServerResponse(false, null, response.message || "Please check your subscription."));
// }
// }
if (!subscriptionData.is_credit && !subscriptionData.is_custom && subscriptionData.subscription_status === "active") {
const updatedCount = parseInt(subscriptionData.current_count) + incrementBy;
const requiredSeats = updatedCount - subscriptionData.quantity;
if (updatedCount > subscriptionData.quantity) {
const obj = {
seats_enough: false,
required_count: requiredSeats,
current_seat_amount: subscriptionData.quantity
};
return res.status(200).send(new ServerResponse(false, obj, null));
}
}
/**
* Checks if the subscription status is in the exclusion list.
* Sends a response if the status is excluded.
*/
if (statusExclude.includes(subscriptionData.subscription_status)) {
return res.status(200).send(new ServerResponse(false, null, "Unable to add user! Please check your subscription status."));
}
/**
* Creates or invites new members based on the request body and user information.
* Sends a response with the result.
*/
const newMembers = await this.createOrInviteMembers(req.body, req.user);
return res.status(200).send(new ServerResponse(true, newMembers, `Your teammates will get an email that gives them access to your team.`).withTitle("Invitations sent"));
}
@HandleExceptions()
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
req.query.field = ["is_owner", "active", "u.name", "u.email"];
req.query.order = "descend";
// Helper function to check for encoded components
function containsEncodedComponents(x: string) {
return decodeURI(x) !== decodeURIComponent(x);
}
// Decode search parameter if it contains encoded components
if (req.query.search && typeof req.query.search === 'string') {
if (containsEncodedComponents(req.query.search)) {
req.query.search = decodeURIComponent(req.query.search);
}
}
const {
searchQuery,
sortField,
sortOrder,
size,
offset
} = this.toPaginationOptions(req.query, ["u.name", "u.email"], true);
const paginate = req.query.all === "false" ? `LIMIT ${size} OFFSET ${offset}` : "";
const q = `
SELECT COUNT(*) AS total,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
FROM (SELECT team_members.id,
(SELECT name
FROM team_member_info_view
WHERE team_member_info_view.team_member_id = team_members.id),
u.avatar_url,
(u.socket_id IS NOT NULL) AS is_online,
(SELECT COUNT(*)
FROM project_members
WHERE team_member_id = team_members.id) AS projects_count,
(SELECT name FROM job_titles WHERE id = team_members.job_title_id) AS job_title,
(SELECT name FROM roles WHERE id = team_members.role_id) AS role_name,
EXISTS(SELECT id
FROM roles
WHERE id = team_members.role_id
AND admin_role IS TRUE) AS is_admin,
(CASE
WHEN user_id = (SELECT user_id FROM teams WHERE id = $1) THEN TRUE
ELSE FALSE END) AS is_owner,
(SELECT email
FROM team_member_info_view
WHERE team_member_info_view.team_member_id = team_members.id) AS email,
EXISTS(SELECT email
FROM email_invitations
WHERE team_member_id = team_members.id
AND email_invitations.team_id = team_members.team_id) AS pending_invitation,
active
FROM team_members
LEFT JOIN users u ON team_members.user_id = u.id
WHERE ${searchQuery} team_id = $1
ORDER BY ${sortField} ${sortOrder} ${paginate}) t) AS data
FROM team_members
LEFT JOIN users u ON team_members.user_id = u.id
WHERE ${searchQuery} team_id = $1
`;
const result = await db.query(q, [req.user?.team_id || null]);
const [members] = result.rows;
members.data?.map((a: any) => {
a.color_code = getColor(a.name);
return a;
});
return res.status(200).send(new ServerResponse(true, members || this.paginatedDatasetDefaultStruct));
}
@HandleExceptions()
public static async getAllMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const q = `SELECT get_team_members($1, $2) AS members;`;
const result = await db.query(q, [req.user?.team_id || null, req.query.project || null]);
const [data] = result.rows;
const members = data?.members || [];
for (const member of members) {
member.color_code = getColor(member.name);
member.usage = +member.usage;
}
return res.status(200).send(new ServerResponse(true, members));
}
@HandleExceptions()
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const q = `
SELECT id,
created_at,
updated_at,
(SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = team_members.id),
(SELECT avatar_url FROM users WHERE id = team_members.user_id),
EXISTS(SELECT email
FROM email_invitations
WHERE team_member_id = team_members.id
AND email_invitations.team_id = team_members.team_id) AS pending_invitation,
(SELECT name FROM job_titles WHERE id = team_members.job_title_id) AS job_title,
COALESCE(
(SELECT email FROM users WHERE id = team_members.user_id),
(SELECT email
FROM email_invitations
WHERE email_invitations.team_member_id = team_members.id
AND email_invitations.team_id = team_members.team_id
LIMIT 1)
) AS email,
EXISTS(SELECT id FROM roles WHERE id = team_members.role_id AND admin_role IS TRUE) AS is_admin
FROM team_members
WHERE id = $1
AND team_id = $2;
`;
const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
const [data] = result.rows;
return res.status(200).send(new ServerResponse(true, data));
}
@HandleExceptions()
public static async getTeamMembersByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const q = `
SELECT project_members.id,
team_member_id,
project_access_level_id,
(SELECT name
FROM project_access_levels
WHERE id = project_access_level_id) AS project_access_level_name,
(SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id),
u.avatar_url,
(SELECT team_member_info_view.email
FROM team_member_info_view
WHERE team_member_info_view.team_member_id = tm.id)
FROM project_members
INNER JOIN team_members tm ON project_members.team_member_id = tm.id
LEFT JOIN users u ON tm.user_id = u.id
WHERE project_id = $1
ORDER BY project_members.created_at DESC;
`;
const result = await db.query(q, [req.params.id]);
return res.status(200).send(new ServerResponse(true, result.rows));
}
@HandleExceptions()
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
req.body.id = req.params.id;
req.body.team_id = req.user?.team_id || null;
req.body.is_admin = !!req.body.is_admin;
const q = `SELECT update_team_member($1) AS team_member;`;
const result = await db.query(q, [JSON.stringify(req.body)]);
return res.status(200).send(new ServerResponse(true, result.rows));
}
@HandleExceptions()
public static async resend_invitation(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
req.body.team_id = req.user?.team_id || null;
const q = `SELECT resend_team_invitation($1) AS invitation;`;
const result = await db.query(q, [JSON.stringify(req.body)]);
const [data] = result.rows;
if (!data?.invitation || !data?.invitation.email)
return res.status(200).send(new ServerResponse(false, null, "Resend failed! Please try again."));
const member = data.invitation;
sendInvitationEmail(
!member.is_new,
req.user as IPassportSession,
!member.is_new ? member.name : member.team_member_id,
member.email,
member.team_member_user_id,
member.name || member.email?.split("@")[0]
);
if (member.team_member_id) {
NotificationsService.sendInvitation(
req.user?.id as string,
req.user?.name as string,
req.user?.team_name as string,
req.user?.team_id as string,
member.team_member_id
);
}
member.id = member.team_member_id;
return res.status(200).send(new ServerResponse(true, null, "Invitation resent"));
}
@HandleExceptions()
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { id } = req.params;
if (!id || !req.user?.team_id) return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
// check subscription status
const subscriptionData = await checkTeamSubscriptionStatus(req.user?.team_id);
if (statusExclude.includes(subscriptionData.subscription_status)) {
return res.status(200).send(new ServerResponse(false, "Please check your subscription status."));
}
const q = `SELECT remove_team_member($1, $2, $3) AS member;`;
const result = await db.query(q, [id, req.user?.id, req.user?.team_id]);
const [data] = result.rows;
const message = `You have been removed from <b>${req.user?.team_name}</b> by <b>${req.user?.name}</b>`;
// if (subscriptionData.status === "trialing") break;
// if (!subscriptionData.is_credit && !subscriptionData.is_custom) {
// if (subscriptionData.subscription_status === "active" && subscriptionData.quantity > 0) {
// const obj = await getActiveTeamMemberCount(req.user?.owner_id ?? "");
// // const activeObj = await getActiveTeamMemberCount(req.user?.owner_id ?? "");
// const userActiveInOtherTeams = await this.checkIfUserActiveInOtherTeams(req.user?.owner_id as string, req.query?.email as string);
// if (!userActiveInOtherTeams) {
// const response = await updateUsers(subscriptionData.subscription_id, obj.user_count);
// if (!response.body.subscription_id) return res.status(200).send(new ServerResponse(false, response.message || "Please check your subscription."));
// }
// }
// }
NotificationsService.sendNotification({
receiver_socket_id: data.socket_id,
message,
team: data.team,
team_id: id
});
IO.emitByUserId(data.member.id, req.user?.id || null, SocketEvents.TEAM_MEMBER_REMOVED, {
teamId: id,
message
});
return res.status(200).send(new ServerResponse(true, result.rows));
}
@HandleExceptions()
public static async getOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const q = `
SELECT (SELECT name FROM projects WHERE id = project_members.project_id) AS name,
(SELECT COUNT(*) FROM tasks_assignees WHERE project_member_id = project_members.id) AS assigned_task_count,
(SELECT COUNT(*)
FROM tasks_assignees
INNER JOIN tasks t ON tasks_assignees.task_id = t.id
INNER JOIN task_statuses ts ON t.status_id = ts.id
WHERE t.archived IS FALSE
AND project_member_id = project_members.id
AND ts.category_id IN
(SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE)) AS done_task_count,
(SELECT COUNT(*)
FROM tasks_assignees
INNER JOIN tasks t ON tasks_assignees.task_id = t.id
INNER JOIN task_statuses ts ON t.status_id = ts.id
WHERE t.archived IS FALSE
AND project_member_id = project_members.id
AND ts.category_id IN
(SELECT id
FROM sys_task_status_categories
WHERE is_doing IS TRUE
OR is_todo IS TRUE)) AS pending_task_count
FROM project_members
WHERE team_member_id = $1;
`;
const result = await db.query(q, [req.params.id]);
for (const object of result.rows) {
object.progress =
object.assigned_task_count > 0
? (
(object.done_task_count / object.assigned_task_count) *
100
).toFixed(0)
: 0;
}
return res.status(200).send(new ServerResponse(true, result.rows));
}
@HandleExceptions()
public static async getOverviewChart(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const q = `
SELECT(SELECT COUNT(*)
FROM tasks_assignees
INNER JOIN tasks t ON tasks_assignees.task_id = t.id
INNER JOIN task_statuses ts ON t.status_id = ts.id
WHERE t.archived IS FALSE AND project_member_id IN
(SELECT id FROM project_members WHERE team_member_id = $1)
AND ts.category_id IN (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE)) AS done_count,
(SELECT COUNT(*)
FROM tasks_assignees
INNER JOIN tasks t ON tasks_assignees.task_id = t.id
INNER JOIN task_statuses ts ON t.status_id = ts.id
WHERE t.archived IS FALSE AND project_member_id IN
(SELECT id FROM project_members WHERE team_member_id = $1)
AND ts.category_id IN
(SELECT id FROM sys_task_status_categories WHERE is_doing IS TRUE OR is_todo IS TRUE)) AS pending_count;
`;
const result = await db.query(q, [req.params.id]);
const [data] = result.rows;
return res.status(200).send(new ServerResponse(true, data));
}
@HandleExceptions()
public static async getTeamMembersTreeMap(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { selected, team, archived } = req.query;
let q = "";
if (selected === "time") {
q = `SELECT ROW_TO_JSON(rec) AS team_members
FROM (SELECT COUNT(*) AS total,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
FROM (SELECT team_members.id,
(SELECT COUNT(*)
FROM project_members
WHERE team_member_id = team_members.id
AND CASE
WHEN ($3 IS TRUE) THEN project_id IS NOT NULL
ELSE project_id NOT IN (SELECT project_id
FROM archived_projects
WHERE archived_projects.project_id = project_members.project_id
AND archived_projects.user_id = $2) END) AS projects_count,
(SELECT SUM(time_spent)
FROM task_work_log
WHERE user_id = team_members.user_id
AND task_id IN (SELECT id
FROM tasks
WHERE project_id IN (SELECT id
FROM projects
WHERE team_id = $1)
AND CASE
WHEN ($3 IS TRUE) THEN project_id IS NOT NULL
ELSE project_id NOT IN (SELECT project_id
FROM archived_projects
WHERE archived_projects.project_id = tasks.project_id
AND archived_projects.user_id = $2) END)) AS time_logged,
(SELECT name
FROM team_member_info_view
WHERE team_member_info_view.team_member_id = team_members.id),
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
FROM (SELECT project_id,
(SELECT name
FROM projects
WHERE projects.id = project_members.project_id),
(SELECT SUM(time_spent)
FROM task_work_log
WHERE task_work_log.task_id IN (SELECT id
FROM tasks
WHERE tasks.project_id = project_members.project_id)
AND task_work_log.user_id IN (SELECT user_id
FROM team_members
WHERE team_member_id = team_members.id)
AND task_id IN (SELECT id
FROM tasks
WHERE id = task_work_log.task_id
AND CASE
WHEN ($3 IS TRUE)
THEN project_id IS NOT NULL
ELSE project_id NOT IN (SELECT project_id
FROM archived_projects
WHERE archived_projects.project_id = tasks.project_id
AND archived_projects.user_id = $2) END)) AS value
FROM project_members
WHERE team_member_id = team_members.id) t) AS projects
FROM team_members
LEFT JOIN users u ON team_members.user_id = u.id
WHERE team_id = $1) t) AS data
FROM team_members
LEFT JOIN users u ON team_members.user_id = u.id
WHERE team_id = $1) rec;`;
}
if (selected === "tasks") {
q = `SELECT ROW_TO_JSON(rec) AS team_members
FROM (SELECT COUNT(*) AS total,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
FROM (SELECT team_members.id,
(SELECT COUNT(*)
FROM project_members
WHERE team_member_id = team_members.id
AND CASE
WHEN ($3 IS FALSE) THEN project_id IS NOT NULL
ELSE project_id NOT IN (SELECT project_id
FROM archived_projects
WHERE archived_projects.project_id = project_members.project_id
AND archived_projects.user_id = $2) END) AS projects_count,
(SELECT COUNT(*)
FROM tasks_assignees
WHERE team_member_id = team_members.id
AND CASE
WHEN ($3 IS FALSE) THEN task_id IN (SELECT id
FROM tasks
WHERE id = tasks_assignees.task_id
AND project_id NOT IN
(SELECT project_id
FROM archived_projects
WHERE archived_projects.project_id = tasks.project_id
AND archived_projects.user_id = $2))
ELSE task_id IS NOT NULL END) AS task_count,
(SELECT name
FROM team_member_info_view
WHERE team_member_info_view.team_member_id = team_members.id),
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
FROM (SELECT project_id,
(SELECT name
FROM projects
WHERE projects.id = project_members.project_id),
(SELECT COUNT(*)
FROM tasks_assignees
WHERE project_member_id = project_members.id) AS value
FROM project_members
WHERE team_member_id = team_members.id
AND CASE
WHEN ($3 IS FALSE) THEN project_id IS NOT NULL
ELSE project_id NOT IN (SELECT project_id
FROM archived_projects
WHERE archived_projects.project_id = project_members.project_id
AND archived_projects.user_id = $2) END) t) AS projects
FROM team_members
LEFT JOIN users u ON team_members.user_id = u.id
WHERE team_id = $1) t) AS DATA
FROM team_members
LEFT JOIN users u
ON team_members.user_id = u.id
WHERE team_id = $1) rec`;
}
const result = await db.query(q, [team, req.user?.id, archived]);
const [data] = result.rows;
const obj: any[] = [];
data.team_members.data.forEach((element: {
id: string;
name: string;
projects_count: number;
task_count: number;
projects: any[];
time_logged: number;
}) => {
obj.push({
id: element.id,
name: element.name,
value: selected === "time" ? element.time_logged || 1 : element.task_count || 0,
color: getColor(element.name) + TEAM_MEMBER_TREE_MAP_COLOR_ALPHA,
label: selected === "time"
? formatDuration(moment.duration(element.time_logged || "0", "seconds"))
: `<br>${element.task_count} total tasks`,
labelToolTip: selected === "time"
? formatDuration(moment.duration(element.time_logged || "0", "seconds"))
: `<b><br> - ${element.projects_count} projects <br> - ${element.task_count} total tasks</br>`
});
if (element.projects.length) {
element.projects.forEach(item => {
obj.push({
id: item.project_id,
name: item.name,
parent: element.id,
value: item.value || 1,
label: selected === "time" ? formatDuration(moment.duration(item.value || "0", "seconds")) : `${item.value} tasks`
});
});
}
});
data.team_members.data = obj;
return res.status(200).send(new ServerResponse(true, data.team_members));
}
@HandleExceptions()
public static async getProjectsByTeamMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { project, status, startDate, endDate } = req.query;
let projectsString, statusString, dateFilterString1, dateFilterString2, dateFilterString3 = "";
if (project && typeof project === "string") {
const projects = project.split(",").map(s => `'${s}'`).join(",");
projectsString = `AND project_id IN (${projects})`;
}
if (status && typeof status === "string") {
const statuses = status.split(",").map(s => `'${s}'`).join(",");
statusString = `AND status_id IN (${statuses})`;
}
if (startDate && endDate) {
dateFilterString1 = `AND twl2.created_at::DATE BETWEEN ${startDate}::DATE AND ${endDate}::DATE) AS total_logged_time`;
dateFilterString2 = `LEFT JOIN tasks t ON p.id = t.project_id LEFT JOIN task_work_log twl ON t.id = twl.task_id`;
dateFilterString3 = `AND twl.user_id = (SELECT user_id FROM team_members WHERE id = project_members.team_member_id)
AND twl.created_at::DATE BETWEEN ${startDate}::DATE AND ${endDate}::DATE;`;
}
const q = `
(SELECT color_code,
name,
(SELECT count(*)
FROM tasks_assignees
WHERE project_members.team_member_id = tasks_assignees.team_member_id
AND task_id IN (SELECT id FROM tasks WHERE tasks.project_id = projects.id)) AS task_count,
(SELECT name FROM teams WHERE teams.id = projects.team_id) AS team,
(SELECT sum(time_spent)
FROM task_work_log
WHERE task_id IN (SELECT id
FROM tasks
WHERE tasks.project_id = projects.id
AND task_work_log.user_id =
(SELECT user_id FROM team_members WHERE id = project_members.team_member_id)) ${dateFilterString1}) AS total_logged_time
FROM project_members
LEFT JOIN projects ON project_id = projects.id
${dateFilterString2}
WHERE team_member_id = $1 ${projectsString} ${statusString} ${dateFilterString3}
ORDER BY name)`;
const result = await db.query(q, [req.params.id]);
result.rows.forEach((element: { total_logged_time: string; }) => {
element.total_logged_time = formatDuration(moment.duration(element.total_logged_time || "0", "seconds"));
});
return res.status(200).send(new ServerResponse(true, result.rows));
}
@HandleExceptions()
public static async getTasksByMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const q = `
SELECT name,
(SELECT COUNT(*)
FROM tasks_assignees
WHERE team_member_info_view.team_member_id = tasks_assignees.team_member_id) ::INT AS y
FROM team_member_info_view
WHERE team_id = $1
ORDER BY name;`;
const result = await db.query(q, [req.user?.team_id]);
return res.status(200).send(new ServerResponse(true, result.rows));
}
public static async getTeamMemberInsightData(team_id: string | undefined, start: any, end: any, project: any, status: any, searchQuery: string, sortField: string, sortOrder: string, size: any, offset: any, all: any) {
let timeRangeTaskWorkLog = "";
let projectsFilterString = "";
let statusFilterString = "";
if (start && end) {
timeRangeTaskWorkLog = `AND EXISTS(SELECT id FROM task_work_log
WHERE created_at::DATE BETWEEN '${start}'::DATE AND '${end}'::DATE
AND task_work_log.user_id = u.id)`;
}
if (project && typeof project === "string") {
const projects = project.split(",").map(s => `'${s}'`).join(",");
projectsFilterString = `AND team_members.id IN (SELECT team_member_id FROM project_members WHERE project_id IN (${projects}))`;
}
if (status && typeof status === "string") {
const projects = status.split(",").map(s => `'${s}'`).join(",");
statusFilterString = `AND team_members.id IN (SELECT team_member_id
FROM project_members
WHERE project_id IN (SELECT id
FROM projects
WHERE projects.team_id = '${team_id}'
AND status_id IN (${projects})))`;
}
const paginate = all === "false" ? `LIMIT ${size} OFFSET ${offset}` : "";
const q = `
SELECT ROW_TO_JSON(rec) AS team_members
FROM (SELECT COUNT(*) AS total,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
FROM (SELECT team_members.id,
(SELECT name
FROM team_member_info_view
WHERE team_member_info_view.team_member_id = team_members.id),
u.avatar_url,
(u.socket_id IS NOT NULL) AS is_online,
(SELECT COUNT(*)
FROM project_members
WHERE team_member_id = team_members.id) AS projects_count,
(SELECT COUNT(*)
FROM tasks_assignees
WHERE team_member_id = team_members.id) AS task_count,
(SELECT SUM(time_spent)
FROM task_work_log
WHERE task_work_log.user_id = tmiv.user_id
AND task_id IN (SELECT id
FROM tasks
WHERE project_id IN (SELECT id
FROM projects
WHERE team_id = $1))) AS total_logged_time_seconds,
(SELECT name FROM job_titles WHERE id = team_members.job_title_id) AS job_title,
(SELECT name FROM roles WHERE id = team_members.role_id) AS role_name,
EXISTS(SELECT id
FROM roles
WHERE id = team_members.role_id
AND admin_role IS TRUE) AS is_admin,
(CASE
WHEN team_members.user_id = (SELECT user_id FROM teams WHERE id = $1) THEN TRUE
ELSE FALSE END) AS is_owner,
(SELECT email
FROM team_member_info_view
WHERE team_member_info_view.team_member_id = team_members.id),
EXISTS(SELECT email
FROM email_invitations
WHERE team_member_id = team_members.id
AND email_invitations.team_id = team_members.team_id) AS pending_invitation,
(SELECT (ARRAY(SELECT NAME
FROM teams
WHERE id IN (SELECT team_id
FROM team_members
WHERE team_members.user_id = tmiv.user_id)))) AS member_teams
FROM team_members
LEFT JOIN users u ON team_members.user_id = u.ID ${timeRangeTaskWorkLog}
LEFT JOIN team_member_info_view tmiv ON team_members.id = tmiv.team_member_id
WHERE team_members.team_id = $1 ${searchQuery} ${timeRangeTaskWorkLog} ${projectsFilterString} ${statusFilterString}
ORDER BY ${sortField} ${sortOrder} ${paginate}) t) AS data
FROM team_members
LEFT JOIN users u ON team_members.user_id = u.ID ${timeRangeTaskWorkLog}
LEFT JOIN team_member_info_view tmiv ON team_members.id = tmiv.team_member_id
WHERE team_members.team_id = $1 ${searchQuery} ${timeRangeTaskWorkLog} ${projectsFilterString} ${statusFilterString}) rec;
`;
const result = await db.query(q, [team_id || null]);
const [data] = result.rows;
return data.team_members;
}
@HandleExceptions()
public static async getTeamMemberList(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const {
searchQuery,
sortField,
sortOrder,
size,
offset
} = this.toPaginationOptions(req.query, ["tmiv.name", "tmiv.email", "u.name"]);
const { start, end, project, status, teamId } = req.query;
const teamMembers = await this.getTeamMemberInsightData(teamId as string, start, end, project, status, searchQuery, sortField, sortOrder, size, offset, req.query.all);
teamMembers.data.map((a: any) => {
a.color_code = getColor(a.name);
a.total_logged_time = formatDuration(moment.duration(a.total_logged_time_seconds || "0", "seconds"));
});
return res.status(200).send(new ServerResponse(true, teamMembers || this.paginatedDatasetDefaultStruct));
}
@HandleExceptions()
public static async getTreeDataByMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { selected, id } = req.query;
let valueString = `(SELECT sum(time_spent)
FROM task_work_log
WHERE task_work_log.task_id IN (SELECT id
FROM tasks
WHERE tasks.project_id = project_members.project_id)
AND task_work_log.user_id IN (SELECT user_id
FROM team_members
WHERE team_member_id = team_members.id))::INT AS value`;
if (selected === "tasks") {
valueString = `(SELECT count(*) FROM tasks_assignees
WHERE project_member_id = project_members.id)::INT AS value`;
}
const q = `
SELECT project_id,
(SELECT name FROM projects WHERE projects.id = project_members.project_id),
(SELECT color_code FROM projects WHERE projects.id = project_members.project_id) AS color,
${valueString}
FROM project_members
WHERE team_member_id = $1`;
const result = await db.query(q, [id]);
const obj: any[] = [];
result.rows.forEach((element: {
project_id: string;
name: string;
value: number;
color: string;
time_logged: number;
}) => {
obj.push({
name: element.name,
value: element.value || 1,
colorValue: element.color + TEAM_MEMBER_TREE_MAP_COLOR_ALPHA,
color: element.color + TEAM_MEMBER_TREE_MAP_COLOR_ALPHA,
label: selected === "tasks" ? `${element.value} tasks` : formatDuration(moment.duration(element.value || "0", "seconds"))
});
});
return res.status(200).send(new ServerResponse(true, obj));
}
@HandleExceptions()
public static async exportAllMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<void> {
const {
searchQuery,
sortField,
sortOrder,
size,
offset
} = this.toPaginationOptions(req.query, ["tmiv.name", "tmiv.email", "u.name"]);
const { start, end, project, status } = req.query;
const teamMembers = await this.getTeamMemberInsightData(req.user?.team_id, start || null, end, project, status, searchQuery, sortField, sortOrder, size, offset, req.query.all);
const exportDate = moment().format("MMM-DD-YYYY");
const fileName = `Worklenz - Team Members Export - ${exportDate}`;
const metadata = {};
const title = "";
const workbook = new Excel.Workbook();
const sheet = workbook.addWorksheet(title);
sheet.headerFooter = {
firstHeader: title
};
sheet.columns = [
{ header: "Name", key: "name", width: 50 },
{ header: "Task Count", key: "task_count", width: 25 },
{ header: "Projects Count", key: "projects_count", width: 25 },
{ header: "Email", key: "email", width: 40 },
];
sheet.getCell("A1").value = req.user?.team_name;
sheet.mergeCells("A1:D1");
sheet.getCell("A1").alignment = { horizontal: "center" };
sheet.getCell("A2").value = `Exported on (${exportDate})`;
sheet.getCell("A2").alignment = { horizontal: "center" };
sheet.getCell("A3").value = `From ${start || "-"} to ${end || "-"}`;
sheet.getRow(5).values = [
"Name",
"Task Count",
"Projects Count",
"Email"
];
for (const item of teamMembers.data) {
const data = {
name: item.name,
task_count: item.task_count,
projects_count: item.projects_count,
email: item.email
};
sheet.addRow(data);
}
sheet.getCell("A1").style.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "D9D9D9" }
};
sheet.getCell("A1").font = {
size: 16
};
sheet.getCell("A2").style.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "F2F2F2" }
};
sheet.getCell("A2").font = {
size: 12
};
sheet.getRow(5).font = {
bold: true
};
res.setHeader("Content-Type", "application/vnd.openxmlformats");
res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
await workbook.xlsx.write(res)
.then(() => {
res.end();
});
}
@HandleExceptions()
public static async exportByMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<void> {
const exportDate = moment().format("MMM-DD-YYYY");
const fileName = `Team Members - ${exportDate}`;
const title = "";
const workbook = new Excel.Workbook();
workbook.addWorksheet(title);
res.setHeader("Content-Type", "application/vnd.openxmlformats");
res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
await workbook.xlsx.write(res)
.then(() => {
res.end();
});
}
@HandleExceptions()
public static async toggleMemberActiveStatus(req: IWorkLenzRequest, res: IWorkLenzResponse) {
if (!req.user?.team_id) return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
// check subscription status
const subscriptionData = await checkTeamSubscriptionStatus(req.user?.team_id);
if (statusExclude.includes(subscriptionData.subscription_status)) {
return res.status(200).send(new ServerResponse(false, "Please check your subscription status."));
}
let data: any;
if (req.query.active === "true") {
const q1 = `SELECT active FROM team_members WHERE id = $1;`;
const result1 = await db.query(q1, [req.params?.id]);
const [status] = result1.rows;
if (status.active) {
const updateQ1 = `UPDATE users
SET active_team = (SELECT id FROM teams WHERE user_id = users.id ORDER BY created_at DESC LIMIT 1)
WHERE id = (SELECT user_id FROM team_members WHERE id = $1 AND active IS TRUE LIMIT 1);`;
await db.query(updateQ1, [req.params?.id]);
}
const q = `UPDATE team_members SET active = NOT active WHERE id = $1 RETURNING active;`;
const result = await db.query(q, [req.params?.id]);
data = result.rows[0];
// const userExists = await this.checkIfUserActiveInOtherTeams(req.user?.owner_id as string, req.query?.email as string);
// if (subscriptionData.status === "trialing") break;
// if (!userExists && !subscriptionData.is_credit && !subscriptionData.is_custom) {
// if (subscriptionData.subscription_status === "active" && subscriptionData.quantity > 0) {
// const operator = req.query.active === "true" ? - 1 : + 1;
// const response = await updateUsers(subscriptionData.subscription_id, subscriptionData.quantity + operator);
// if (!response.body.subscription_id) return res.status(200).send(new ServerResponse(false, response.message || "Please check your subscription."));
// }
// }
} else {
const userExists = await this.checkIfUserActiveInOtherTeams(req.user?.owner_id as string, req.query?.email as string);
// if (subscriptionData.status === "trialing") break;
// if (!userExists && !subscriptionData.is_credit && !subscriptionData.is_custom) {
// if (subscriptionData.subscription_status === "active" && subscriptionData.quantity > 0) {
// const operator = req.query.active === "true" ? - 1 : + 1;
// const response = await updateUsers(subscriptionData.subscription_id, subscriptionData.quantity + operator);
// if (!response.body.subscription_id) return res.status(200).send(new ServerResponse(false, response.message || "Please check your subscription."));
// }
// }
const q1 = `SELECT active FROM team_members WHERE id = $1;`;
const result1 = await db.query(q1, [req.params?.id]);
const [status] = result1.rows;
if (status.active) {
const updateQ1 = `UPDATE users
SET active_team = (SELECT id FROM teams WHERE user_id = users.id ORDER BY created_at DESC LIMIT 1)
WHERE id = (SELECT user_id FROM team_members WHERE id = $1 AND active IS TRUE LIMIT 1);`;
await db.query(updateQ1, [req.params?.id]);
}
const q = `UPDATE team_members SET active = NOT active WHERE id = $1 RETURNING active;`;
const result = await db.query(q, [req.params?.id]);
data = result.rows[0];
}
return res.status(200).send(new ServerResponse(true, [], `Team member ${data.active ? " activated" : " deactivated"} successfully.`));
}
@HandleExceptions({
raisedExceptions: {
"ERROR_EMAIL_INVITATION_EXISTS": `Team member with email "{0}" already exists.`
}
})
public static async addTeamMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
req.body.team_id = req.params?.id || null;
if (!req.body.team_id || !req.user?.id) return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
// check the subscription status
const subscriptionData = await checkTeamSubscriptionStatus(req.body.team_id);
if (statusExclude.includes(subscriptionData.subscription_status)) {
return res.status(200).send(new ServerResponse(false, "Please check your subscription status."));
}
// if (subscriptionData.status === "trialing") break;
if (!subscriptionData.is_credit && !subscriptionData.is_custom) {
if (subscriptionData.subscription_status === "active") {
const response = await updateUsers(subscriptionData.subscription_id, subscriptionData.quantity + (req.body.emails.length || 1));
if (!response.body.subscription_id) return res.status(200).send(new ServerResponse(false, response.message || "Please check your subscription."));
}
}
const newMembers = await this.createOrInviteMembers(req.body, req.user);
return res.status(200).send(new ServerResponse(true, newMembers, `Your teammates will get an email that gives them access to your team.`).withTitle("Invitations sent"));
}
}