- Introduced methods to calculate percentage, convert time formats, and determine date-related metrics such as days left, overdue status, and if a date is today. - Updated existing logic in getSingleMemberProjects to utilize the new utility methods for improved clarity and maintainability.
1396 lines
64 KiB
TypeScript
1396 lines
64 KiB
TypeScript
import moment from "moment";
|
|
import db from "../../config/db";
|
|
import HandleExceptions from "../../decorators/handle-exceptions";
|
|
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
|
|
import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
|
|
import { ServerResponse } from "../../models/server-response";
|
|
import { DATE_RANGES, TASK_PRIORITY_COLOR_ALPHA } from "../../shared/constants";
|
|
import { formatDuration, getColor, int } from "../../shared/utils";
|
|
import ReportingControllerBaseWithTimezone from "./reporting-controller-base-with-timezone";
|
|
import Excel from "exceljs";
|
|
|
|
export default class ReportingMembersController extends ReportingControllerBaseWithTimezone {
|
|
|
|
protected static getPercentage(n: number, total: number) {
|
|
return +(n ? (n / total) * 100 : 0).toFixed();
|
|
}
|
|
|
|
protected static getCurrentTeamId(req: IWorkLenzRequest): string | null {
|
|
return req.user?.team_id ?? null;
|
|
}
|
|
|
|
public static convertMinutesToHoursAndMinutes(totalMinutes: number) {
|
|
const hours = Math.floor(totalMinutes / 60);
|
|
const minutes = totalMinutes % 60;
|
|
return `${hours}h ${minutes}m`;
|
|
}
|
|
|
|
public static convertSecondsToHoursAndMinutes(seconds: number) {
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
return `${hours}h ${minutes}m`;
|
|
}
|
|
|
|
protected static formatEndDate(endDate: string) {
|
|
const end = moment(endDate).format("YYYY-MM-DD");
|
|
const fEndDate = moment(end);
|
|
return fEndDate;
|
|
}
|
|
|
|
protected static formatCurrentDate() {
|
|
const current = moment().format("YYYY-MM-DD");
|
|
const fCurrentDate = moment(current);
|
|
return fCurrentDate;
|
|
}
|
|
|
|
protected static getDaysLeft(endDate: string): number | null {
|
|
if (!endDate) return null;
|
|
|
|
const fCurrentDate = this.formatCurrentDate();
|
|
const fEndDate = this.formatEndDate(endDate);
|
|
|
|
return fEndDate.diff(fCurrentDate, "days");
|
|
}
|
|
|
|
protected static isOverdue(endDate: string): boolean {
|
|
if (!endDate) return false;
|
|
|
|
const fCurrentDate = this.formatCurrentDate();
|
|
const fEndDate = this.formatEndDate(endDate);
|
|
|
|
return fEndDate.isBefore(fCurrentDate);
|
|
}
|
|
|
|
protected static isToday(endDate: string): boolean {
|
|
if (!endDate) return false;
|
|
|
|
const fCurrentDate = this.formatCurrentDate();
|
|
const fEndDate = this.formatEndDate(endDate);
|
|
|
|
return fEndDate.isSame(fCurrentDate);
|
|
}
|
|
|
|
private static async getMembers(
|
|
teamId: string, searchQuery = "",
|
|
size: number | null = null,
|
|
offset: number | null = null,
|
|
teamsClause = "",
|
|
key = DATE_RANGES.LAST_WEEK,
|
|
dateRange: string[] = [],
|
|
includeArchived: boolean,
|
|
userId: string
|
|
) {
|
|
const pagingClause = (size !== null && offset !== null) ? `LIMIT ${size} OFFSET ${offset}` : "";
|
|
const archivedClause = includeArchived
|
|
? ""
|
|
: `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`;
|
|
|
|
// const durationFilterClause = this.memberTasksDurationFilter(key, dateRange);
|
|
const assignClause = this.memberAssignDurationFilter(key, dateRange);
|
|
const completedDurationClasue = this.completedDurationFilter(key, dateRange);
|
|
const overdueActivityLogsClause = this.getActivityLogsOverdue(key, dateRange);
|
|
const activityLogCreationFilter = this.getActivityLogsCreationClause(key, dateRange);
|
|
|
|
const q = `SELECT COUNT(DISTINCT email) AS total,
|
|
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
|
FROM (SELECT team_member_id AS id,
|
|
name,
|
|
avatar_url,
|
|
email,
|
|
(SELECT COUNT(project_id)
|
|
FROM project_members pm
|
|
WHERE pm.team_member_id = tmiv.team_member_id) AS projects,
|
|
|
|
(SELECT GREATEST(
|
|
(SELECT MAX(created_at) FROM task_activity_logs WHERE user_id = (SELECT user_id FROM team_members WHERE id = tmiv.team_member_id) AND team_id = $1),
|
|
(SELECT MAX(created_at) FROM task_work_log WHERE user_id = (SELECT user_id FROM team_members WHERE id = tmiv.team_member_id)
|
|
AND task_id IN (SELECT id FROM tasks t WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1 )
|
|
)))) AS last_user_activity,
|
|
|
|
(SELECT COUNT(*)
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
|
WHERE team_member_id = tmiv.team_member_id ${archivedClause}) AS total_tasks,
|
|
|
|
(SELECT COUNT(*)
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
|
WHERE team_member_id = tmiv.team_member_id ${assignClause} ${archivedClause}) AS tasks,
|
|
|
|
(SELECT COUNT(*)
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
|
WHERE team_member_id = tmiv.team_member_id
|
|
AND is_completed(status_id, t.project_id) ${archivedClause}) AS total_completed,
|
|
|
|
(SELECT COUNT(*) FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
|
WHERE team_member_id = tmiv.team_member_id AND is_completed(status_id, t.project_id) ${completedDurationClasue} ${archivedClause}) AS completed,
|
|
|
|
(SELECT COUNT(*)
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
|
WHERE team_member_id = tmiv.team_member_id
|
|
AND is_doing(status_id, t.project_id) ${archivedClause}) AS total_ongoing,
|
|
|
|
(SELECT COUNT(*)
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
|
WHERE team_member_id = tmiv.team_member_id
|
|
AND is_doing(status_id, t.project_id) ${archivedClause}) AS ongoing,
|
|
|
|
(SELECT COUNT(*) FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
|
WHERE team_member_id = tmiv.team_member_id ${overdueActivityLogsClause} ${archivedClause}) AS overdue,
|
|
|
|
(SELECT COUNT(*)
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
|
WHERE team_member_id = tmiv.team_member_id
|
|
AND is_todo(status_id, t.project_id) ${archivedClause}) AS todo,
|
|
|
|
(SELECT COUNT(*)
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
|
WHERE team_member_id = tmiv.team_member_id
|
|
AND is_todo((SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' ${activityLogCreationFilter} ORDER BY tl.created_at DESC LIMIT 1)::UUID, t.project_id) ${archivedClause}) AS todo_by_activity_logs,
|
|
|
|
(SELECT COUNT(*)
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
|
WHERE team_member_id = tmiv.team_member_id
|
|
AND is_doing((SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' ${activityLogCreationFilter} ORDER BY tl.created_at DESC LIMIT 1)::UUID, t.project_id) ${archivedClause}) AS ongoing_by_activity_logs
|
|
FROM team_member_info_view tmiv
|
|
WHERE tmiv.team_id = $1 ${teamsClause}
|
|
AND tmiv.team_member_id IN (SELECT team_member_id
|
|
FROM project_members
|
|
WHERE project_id IN (SELECT id FROM projects WHERE projects.team_id = tmiv.team_id))
|
|
${searchQuery}
|
|
GROUP BY email, name, avatar_url, team_member_id, tmiv.team_id
|
|
ORDER BY last_user_activity DESC NULLS LAST
|
|
|
|
${pagingClause}) t) AS members
|
|
FROM team_member_info_view tmiv
|
|
WHERE tmiv.team_id = $1 ${teamsClause}
|
|
AND tmiv.team_member_id IN (SELECT team_member_id
|
|
FROM project_members
|
|
WHERE project_id IN (SELECT id FROM projects WHERE projects.team_id = tmiv.team_id))
|
|
${searchQuery}`;
|
|
const result = await db.query(q, [teamId]);
|
|
const [data] = result.rows;
|
|
|
|
for (const member of data.members) {
|
|
member.color_code = getColor(member.name) + TASK_PRIORITY_COLOR_ALPHA;
|
|
member.tasks_stat = {
|
|
todo: this.getPercentage(int(member.todo_by_activity_logs), + (member.completed + member.todo_by_activity_logs + member.ongoing_by_activity_logs)),
|
|
doing: this.getPercentage(int(member.ongoing_by_activity_logs), + (member.completed + member.todo_by_activity_logs + member.ongoing_by_activity_logs)),
|
|
done: this.getPercentage(int(member.completed), + (member.completed + member.todo_by_activity_logs + member.ongoing_by_activity_logs))
|
|
};
|
|
member.member_teams = this.createTagList(member.member_teams, 2);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
private static flatString(text: string) {
|
|
return (text || "").split(" ").map(s => `'${s}'`).join(",");
|
|
}
|
|
|
|
protected static memberTasksDurationFilter(key: string, dateRange: string[]) {
|
|
if (dateRange.length === 2) {
|
|
const start = moment(dateRange[0]).format("YYYY-MM-DD");
|
|
const end = moment(dateRange[1]).format("YYYY-MM-DD");
|
|
|
|
if (start === end) {
|
|
return `AND t.end_date::DATE = '${start}'::DATE`;
|
|
}
|
|
|
|
return `AND t.end_date::DATE >= '${start}'::DATE AND t.end_date::DATE <= '${end}'::DATE`;
|
|
}
|
|
|
|
if (key === DATE_RANGES.YESTERDAY)
|
|
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE`;
|
|
if (key === DATE_RANGES.LAST_WEEK)
|
|
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
|
if (key === DATE_RANGES.LAST_MONTH)
|
|
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
|
if (key === DATE_RANGES.LAST_QUARTER)
|
|
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
|
|
|
return "";
|
|
}
|
|
|
|
protected static memberAssignDurationFilter(key: string, dateRange: string[]) {
|
|
if (dateRange.length === 2) {
|
|
const start = moment(dateRange[0]).format("YYYY-MM-DD");
|
|
const end = moment(dateRange[1]).format("YYYY-MM-DD");
|
|
|
|
if (start === end) {
|
|
return `AND ta.updated_at::DATE = '${start}'::DATE`;
|
|
}
|
|
|
|
return `AND ta.updated_at::DATE >= '${start}'::DATE AND ta.updated_at::DATE <= '${end}'::DATE`;
|
|
}
|
|
|
|
if (key === DATE_RANGES.YESTERDAY)
|
|
return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE`;
|
|
if (key === DATE_RANGES.LAST_WEEK)
|
|
return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
|
if (key === DATE_RANGES.LAST_MONTH)
|
|
return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
|
if (key === DATE_RANGES.LAST_QUARTER)
|
|
return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
|
|
|
return "";
|
|
}
|
|
|
|
protected static completedDurationFilter(key: string, dateRange: string[]) {
|
|
if (dateRange.length === 2) {
|
|
const start = moment(dateRange[0]).format("YYYY-MM-DD");
|
|
const end = moment(dateRange[1]).format("YYYY-MM-DD");
|
|
|
|
if (start === end) {
|
|
return `AND t.completed_at::DATE = '${start}'::DATE`;
|
|
}
|
|
|
|
return `AND t.completed_at::DATE >= '${start}'::DATE AND t.completed_at::DATE <= '${end}'::DATE`;
|
|
}
|
|
|
|
if (key === DATE_RANGES.YESTERDAY)
|
|
return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE`;
|
|
if (key === DATE_RANGES.LAST_WEEK)
|
|
return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
|
if (key === DATE_RANGES.LAST_MONTH)
|
|
return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
|
if (key === DATE_RANGES.LAST_QUARTER)
|
|
return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
|
|
|
return "";
|
|
}
|
|
|
|
protected static getOverdueClause(key: string, dateRange: string[]) {
|
|
|
|
if (dateRange.length === 2) {
|
|
const start = moment(dateRange[0]).format("YYYY-MM-DD");
|
|
const end = moment(dateRange[1]).format("YYYY-MM-DD");
|
|
|
|
if (start === end) {
|
|
return `AND t.end_date::DATE = '${start}'::DATE`;
|
|
}
|
|
|
|
return `AND t.end_date::DATE >= '${start}'::DATE AND t.end_date::DATE <= '${end}'::DATE`;
|
|
}
|
|
|
|
if (key === DATE_RANGES.YESTERDAY)
|
|
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND t.end_date::DATE < NOW()::DATE`;
|
|
if (key === DATE_RANGES.LAST_WEEK)
|
|
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND t.end_date::DATE < NOW()::DATE`;
|
|
if (key === DATE_RANGES.LAST_MONTH)
|
|
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND t.end_date::DATE < NOW()::DATE`;
|
|
if (key === DATE_RANGES.LAST_QUARTER)
|
|
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND t.end_date::DATE < NOW()::DATE`;
|
|
|
|
|
|
return ` AND t.end_date::DATE < NOW()::DATE `;
|
|
}
|
|
|
|
protected static getTaskSelectorClause() {
|
|
return `SELECT t.id,
|
|
t.name,
|
|
t.project_id,
|
|
(SELECT name FROM projects WHERE id = t.project_id) AS project_name,
|
|
t.parent_task_id,
|
|
t.parent_task_id IS NOT NULL AS is_sub_task,
|
|
|
|
t.end_date,
|
|
t.completed_at,
|
|
|
|
(CASE
|
|
WHEN (CURRENT_DATE::DATE > end_date::DATE AND
|
|
status_id IN (SELECT id
|
|
FROM task_statuses
|
|
WHERE project_id = t.project_id
|
|
AND category_id IN
|
|
(SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE)))
|
|
THEN CURRENT_DATE::DATE - end_date::DATE
|
|
ELSE 0 END) AS days_overdue,
|
|
|
|
(SELECT name FROM task_statuses WHERE id = t.status_id) AS status_name,
|
|
(SELECT color_code FROM sys_task_status_categories WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id AND t.project_id = t.project_id)) AS status_color,
|
|
|
|
(SELECT name FROM task_priorities WHERE id = t.priority_id) AS priority_name,
|
|
(SELECT color_code FROM task_priorities WHERE id = t.priority_id) AS priority_color,
|
|
|
|
(SELECT name FROM project_phases WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_name,
|
|
(SELECT color_code FROM project_phases WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_color,
|
|
|
|
(total_minutes * 60) AS total_minutes,
|
|
(SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id AND ta.team_member_id = $1) AS time_logged,
|
|
((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id AND ta.team_member_id = $1) - (total_minutes * 60)) AS overlogged_time`;
|
|
}
|
|
|
|
protected static getActivityLogsOverdue(key: string, dateRange: string[]) {
|
|
|
|
if (dateRange.length === 2) {
|
|
const end = moment(dateRange[1]).format("YYYY-MM-DD");
|
|
return `AND is_overdue_for_date(t.id, '${end}'::DATE)`;
|
|
}
|
|
|
|
return `AND is_overdue_for_date(t.id, NOW()::DATE)`;
|
|
}
|
|
|
|
protected static getActivityLogsCreationClause(key: string, dateRange: string[]) {
|
|
if (dateRange.length === 2) {
|
|
const end = moment(dateRange[1]).format("YYYY-MM-DD");
|
|
return `AND tl.created_at::DATE <= '${end}'::DATE`;
|
|
}
|
|
return `AND tl.created_at::DATE <= NOW()::DATE`;
|
|
}
|
|
|
|
protected static getDateRangeClauseMembers(key: string, dateRange: string[], tableAlias: string) {
|
|
if (dateRange.length === 2) {
|
|
const start = moment(dateRange[0]).format("YYYY-MM-DD");
|
|
const end = moment(dateRange[1]).format("YYYY-MM-DD");
|
|
|
|
if (start === end) {
|
|
return `AND ${tableAlias}.created_at::DATE = '${start}'::DATE`;
|
|
}
|
|
|
|
return `AND ${tableAlias}.created_at::DATE >= '${start}'::DATE AND ${tableAlias}.created_at < '${end}'::DATE + INTERVAL '1 day'`;
|
|
}
|
|
|
|
if (key === DATE_RANGES.YESTERDAY)
|
|
return `AND ${tableAlias}.created_at >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND ${tableAlias}.created_at < CURRENT_DATE::DATE`;
|
|
if (key === DATE_RANGES.LAST_WEEK)
|
|
return `AND ${tableAlias}.created_at >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND ${tableAlias}.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
|
if (key === DATE_RANGES.LAST_MONTH)
|
|
return `AND ${tableAlias}.created_at >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND ${tableAlias}.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
|
if (key === DATE_RANGES.LAST_QUARTER)
|
|
return `AND ${tableAlias}.created_at >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND ${tableAlias}.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
|
|
|
return "";
|
|
}
|
|
|
|
private static formatDuration(duration: moment.Duration) {
|
|
const empty = "0h 0m";
|
|
let format = "";
|
|
|
|
if (duration.asMilliseconds() === 0) return empty;
|
|
|
|
const h = ~~(duration.asHours());
|
|
const m = duration.minutes();
|
|
const s = duration.seconds();
|
|
|
|
if (h === 0 && s > 0) {
|
|
format = `${m}m ${s}s`;
|
|
} else if (h > 0 && s === 0) {
|
|
format = `${h}h ${m}m`;
|
|
} else if (h > 0 && s > 0) {
|
|
format = `${h}h ${m}m ${s}s`;
|
|
} else {
|
|
format = `${h}h ${m}m`;
|
|
}
|
|
|
|
return format;
|
|
}
|
|
|
|
@HandleExceptions()
|
|
public static async getReportingMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
|
const { searchQuery, size, offset } = this.toPaginationOptions(req.query, ["name"]);
|
|
const { duration, date_range } = req.query;
|
|
const archived = req.query.archived === "true";
|
|
|
|
let dateRange: string[] = [];
|
|
if (typeof date_range === "string") {
|
|
dateRange = date_range.split(",");
|
|
}
|
|
|
|
const teamsClause =
|
|
req.query.teams as string
|
|
? `AND tmiv.team_id IN (${this.flatString(req.query.teams as string)})`
|
|
: "";
|
|
|
|
const teamId = this.getCurrentTeamId(req);
|
|
const result = await this.getMembers(teamId as string, searchQuery, size, offset, teamsClause, duration as string, dateRange, archived, req.user?.id as string);
|
|
const body = {
|
|
total: result.total,
|
|
members: result.members,
|
|
team: {
|
|
id: req.user?.team_id,
|
|
name: req.user?.team_name
|
|
}
|
|
};
|
|
return res.status(200).send(new ServerResponse(true, body));
|
|
}
|
|
|
|
public static formatDurationDate = (date: Date) => {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
return `${year}-${month}-${day}`;
|
|
};
|
|
|
|
@HandleExceptions()
|
|
public static async export(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
|
const { search } = req.body;
|
|
const { duration, date_range } = req.query;
|
|
const archived = req.query.archived === "true";
|
|
|
|
let dateRange: string[] = [];
|
|
if (typeof date_range === "string") {
|
|
dateRange = date_range.split(",");
|
|
}
|
|
|
|
const teamId = this.getCurrentTeamId(req);
|
|
const teamName = (req.query.team_name as string)?.trim() || null;
|
|
const result = await this.getMembers(teamId as string, "", null, null, "", duration as string, dateRange, archived, req.user?.id as string);
|
|
|
|
let start = "-";
|
|
let end = "-";
|
|
|
|
if (dateRange.length === 2) {
|
|
start = dateRange[0] ? this.formatDurationDate(new Date(dateRange[0])).toString() : "-";
|
|
end = dateRange[1] ? this.formatDurationDate(new Date(dateRange[1])).toString() : "-";
|
|
} else {
|
|
switch (duration) {
|
|
case DATE_RANGES.YESTERDAY:
|
|
start = moment().subtract(1, "day").format("YYYY-MM-DD").toString();
|
|
break;
|
|
case DATE_RANGES.LAST_WEEK:
|
|
start = moment().subtract(1, "week").format("YYYY-MM-DD").toString();
|
|
break;
|
|
case DATE_RANGES.LAST_MONTH:
|
|
start = moment().subtract(1, "month").format("YYYY-MM-DD").toString();
|
|
break;
|
|
case DATE_RANGES.LAST_QUARTER:
|
|
start = moment().subtract(3, "months").format("YYYY-MM-DD").toString();
|
|
break;
|
|
}
|
|
end = moment().format("YYYY-MM-DD").toString();
|
|
}
|
|
|
|
// excel file
|
|
const exportDate = moment().format("MMM-DD-YYYY");
|
|
const fileName = `${teamName} members - ${exportDate}`;
|
|
const workbook = new Excel.Workbook();
|
|
|
|
const sheet = workbook.addWorksheet("Members");
|
|
|
|
sheet.columns = [
|
|
{ header: "Member", key: "name", width: 30 },
|
|
{ header: "Email", key: "email", width: 20 },
|
|
{ header: "Tasks Assigned", key: "tasks", width: 20 },
|
|
{ header: "Overdue Tasks", key: "overdue_tasks", width: 20 },
|
|
{ header: "Completed Tasks", key: "completed_tasks", width: 20 },
|
|
{ header: "Ongoing Tasks", key: "ongoing_tasks", width: 20 },
|
|
{ header: "Done Tasks(%)", key: "done_tasks", width: 20 },
|
|
{ header: "Doing Tasks(%)", key: "doing_tasks", width: 20 },
|
|
{ header: "Todo Tasks(%)", key: "todo_tasks", width: 20 }
|
|
];
|
|
|
|
// set title
|
|
sheet.getCell("A1").value = `Members from ${teamName}`;
|
|
sheet.mergeCells("A1:K1");
|
|
sheet.getCell("A1").alignment = { horizontal: "center" };
|
|
sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
|
|
sheet.getCell("A1").font = { size: 16 };
|
|
|
|
// set export date
|
|
sheet.getCell("A2").value = `Exported on : ${exportDate}`;
|
|
sheet.mergeCells("A2:K2");
|
|
sheet.getCell("A2").alignment = { horizontal: "center" };
|
|
sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
|
|
sheet.getCell("A2").font = { size: 12 };
|
|
|
|
// set duration
|
|
sheet.getCell("A3").value = `From : ${start} To : ${end}`;
|
|
sheet.mergeCells("A3:D3");
|
|
|
|
// set table headers
|
|
sheet.getRow(5).values = ["Member", "Email", "Tasks Assigned", "Overdue Tasks", "Completed Tasks", "Ongoing Tasks", "Done Tasks(%)", "Doing Tasks(%)", "Todo Tasks(%)"];
|
|
sheet.getRow(5).font = { bold: true };
|
|
|
|
for (const member of result.members) {
|
|
sheet.addRow({
|
|
name: member.name,
|
|
email: member.email,
|
|
tasks: member.tasks,
|
|
overdue_tasks: member.overdue,
|
|
completed_tasks: member.completed,
|
|
ongoing_tasks: member.ongoing,
|
|
done_tasks: member.completed,
|
|
doing_tasks: member.ongoing_by_activity_logs,
|
|
todo_tasks: member.todo_by_activity_logs
|
|
});
|
|
}
|
|
|
|
// download excel
|
|
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 exportTimeLogs(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
|
|
|
const { duration, date_range, team_id, team_member_id } = req.query;
|
|
|
|
const includeArchived = req.query.archived === "true";
|
|
|
|
let dateRange: string[] = [];
|
|
if (typeof date_range === "string") {
|
|
dateRange = date_range.split(",");
|
|
}
|
|
|
|
// Get user timezone for proper date filtering
|
|
const userTimezone = await this.getUserTimezone(req.user?.id as string);
|
|
const durationClause = this.getDateRangeClauseWithTimezone(duration as string || DATE_RANGES.LAST_WEEK, dateRange, userTimezone);
|
|
const minMaxDateClause = this.getMinMaxDates(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "task_work_log");
|
|
const memberName = (req.query.member_name as string)?.trim() || null;
|
|
|
|
const logGroups = await this.memberTimeLogsData(durationClause, minMaxDateClause, team_id as string, team_member_id as string, includeArchived, req.user?.id as string);
|
|
|
|
let start = "-";
|
|
let end = "-";
|
|
|
|
if (dateRange.length === 2) {
|
|
start = dateRange[0] ? this.formatDurationDate(new Date(dateRange[0])).toString() : "-";
|
|
end = dateRange[1] ? this.formatDurationDate(new Date(dateRange[1])).toString() : "-";
|
|
} else {
|
|
switch (duration) {
|
|
case DATE_RANGES.YESTERDAY:
|
|
start = moment().subtract(1, "day").format("YYYY-MM-DD").toString();
|
|
break;
|
|
case DATE_RANGES.LAST_WEEK:
|
|
start = moment().subtract(1, "week").format("YYYY-MM-DD").toString();
|
|
break;
|
|
case DATE_RANGES.LAST_MONTH:
|
|
start = moment().subtract(1, "month").format("YYYY-MM-DD").toString();
|
|
break;
|
|
case DATE_RANGES.LAST_QUARTER:
|
|
start = moment().subtract(3, "months").format("YYYY-MM-DD").toString();
|
|
break;
|
|
}
|
|
end = moment().format("YYYY-MM-DD").toString();
|
|
}
|
|
|
|
|
|
const exportDate = moment().format("MMM-DD-YYYY");
|
|
const fileName = `${memberName} timelogs - ${exportDate}`;
|
|
const workbook = new Excel.Workbook();
|
|
|
|
const sheet = workbook.addWorksheet("Members");
|
|
|
|
sheet.columns = [
|
|
{ header: "Date", key: "date", width: 30 },
|
|
{ header: "Log", key: "log", width: 120 },
|
|
];
|
|
|
|
sheet.getCell("A1").value = `Timelogs of ${memberName}`;
|
|
sheet.mergeCells("A1:K1");
|
|
sheet.getCell("A1").alignment = { horizontal: "center" };
|
|
sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
|
|
sheet.getCell("A1").font = { size: 16 };
|
|
|
|
// set export date
|
|
sheet.getCell("A2").value = `Exported on : ${exportDate}`;
|
|
sheet.mergeCells("A2:K2");
|
|
sheet.getCell("A2").alignment = { horizontal: "center" };
|
|
sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
|
|
sheet.getCell("A2").font = { size: 12 };
|
|
|
|
// set duration
|
|
sheet.getCell("A3").value = `From : ${start} To : ${end}`;
|
|
sheet.mergeCells("A3:D3");
|
|
|
|
// set table headers
|
|
sheet.getRow(5).values = ["Time Logs"];
|
|
sheet.getRow(5).font = { bold: true };
|
|
|
|
for (const row of logGroups) {
|
|
for (const log of row.logs) {
|
|
sheet.addRow({
|
|
date: row.log_day,
|
|
log: `Logged ${log.time_spent_string} for ${log.task_name} in ${log.project_name}`
|
|
});
|
|
}
|
|
}
|
|
|
|
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 exportActivityLogs(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
|
|
|
const { duration, date_range, team_id, team_member_id } = req.query;
|
|
const includeArchived = req.query.archived === "true";
|
|
|
|
let dateRange: string[] = [];
|
|
if (typeof date_range === "string") {
|
|
dateRange = date_range.split(",");
|
|
}
|
|
|
|
const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "tal");
|
|
const minMaxDateClause = this.getMinMaxDates(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "task_activity_logs");
|
|
const memberName = (req.query.member_name as string)?.trim() || null;
|
|
|
|
const logGroups = await this.memberActivityLogsData(durationClause, minMaxDateClause, team_id as string, team_member_id as string, includeArchived, req.user?.id as string);
|
|
|
|
let start = "-";
|
|
let end = "-";
|
|
|
|
if (dateRange.length === 2) {
|
|
start = dateRange[0] ? this.formatDurationDate(new Date(dateRange[0])).toString() : "-";
|
|
end = dateRange[1] ? this.formatDurationDate(new Date(dateRange[1])).toString() : "-";
|
|
} else {
|
|
switch (duration) {
|
|
case DATE_RANGES.YESTERDAY:
|
|
start = moment().subtract(1, "day").format("YYYY-MM-DD").toString();
|
|
break;
|
|
case DATE_RANGES.LAST_WEEK:
|
|
start = moment().subtract(1, "week").format("YYYY-MM-DD").toString();
|
|
break;
|
|
case DATE_RANGES.LAST_MONTH:
|
|
start = moment().subtract(1, "month").format("YYYY-MM-DD").toString();
|
|
break;
|
|
case DATE_RANGES.LAST_QUARTER:
|
|
start = moment().subtract(3, "months").format("YYYY-MM-DD").toString();
|
|
break;
|
|
}
|
|
end = moment().format("YYYY-MM-DD").toString();
|
|
}
|
|
|
|
|
|
const exportDate = moment().format("MMM-DD-YYYY");
|
|
const fileName = `${memberName} activitylogs - ${exportDate}`;
|
|
const workbook = new Excel.Workbook();
|
|
|
|
const sheet = workbook.addWorksheet("Members");
|
|
|
|
sheet.columns = [
|
|
{ header: "Date", key: "date", width: 30 },
|
|
{ header: "Log", key: "log", width: 120 },
|
|
];
|
|
|
|
sheet.getCell("A1").value = `Activities of ${memberName}`;
|
|
sheet.mergeCells("A1:K1");
|
|
sheet.getCell("A1").alignment = { horizontal: "center" };
|
|
sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
|
|
sheet.getCell("A1").font = { size: 16 };
|
|
|
|
// set export date
|
|
sheet.getCell("A2").value = `Exported on : ${exportDate}`;
|
|
sheet.mergeCells("A2:K2");
|
|
sheet.getCell("A2").alignment = { horizontal: "center" };
|
|
sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
|
|
sheet.getCell("A2").font = { size: 12 };
|
|
|
|
// set duration
|
|
sheet.getCell("A3").value = `From : ${start} To : ${end}`;
|
|
sheet.mergeCells("A3:D3");
|
|
|
|
// set table headers
|
|
sheet.getRow(5).values = ["Activity Logs"];
|
|
sheet.getRow(5).font = { bold: true };
|
|
|
|
for (const row of logGroups) {
|
|
for (const log of row.logs) {
|
|
!log.previous ? log.previous = "NULL" : log.previous;
|
|
!log.current ? log.current = "NULL" : log.current;
|
|
switch (log.attribute_type) {
|
|
case "start_date":
|
|
log.attribute_type = "Start Date";
|
|
break;
|
|
case "end_date":
|
|
log.attribute_type = "End Date";
|
|
break;
|
|
case "status":
|
|
log.attribute_type = "Status";
|
|
break;
|
|
case "priority":
|
|
log.attribute_type = "Priority";
|
|
break;
|
|
case "phase":
|
|
log.attribute_type = "Phase";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
sheet.addRow({
|
|
date: row.log_day,
|
|
log: `Updated ${log.attribute_type} from ${log.previous} to ${log.current} in ${log.task_name} within ${log.project_name}.`
|
|
});
|
|
}
|
|
}
|
|
|
|
res.setHeader("Content-Type", "application/vnd.openxmlformats");
|
|
res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
|
|
|
|
await workbook.xlsx.write(res)
|
|
.then(() => {
|
|
res.end();
|
|
});
|
|
|
|
}
|
|
|
|
|
|
public static async getMemberProjectsData(teamId: string, teamMemberId: string, searchQuery: string, archived: boolean, userId: string) {
|
|
|
|
const teamClause = teamId
|
|
? `team_member_id = '${teamMemberId as string}'`
|
|
: `team_member_id IN (SELECT team_member_id
|
|
FROM team_member_info_view tmiv
|
|
WHERE email = (SELECT email
|
|
FROM team_member_info_view tmiv2
|
|
WHERE tmiv2.team_member_id = '${teamMemberId}' AND in_organization(p.team_id, tmiv2.team_id)))`;
|
|
|
|
const archivedClause = archived ? `` : ` AND pm.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = pm.project_id AND user_id = '${userId}')`;
|
|
|
|
const q = `SELECT p.id, p.name, pm.team_member_id,
|
|
(SELECT name FROM teams WHERE id = p.team_id) AS team,
|
|
(SELECT COUNT(task_id)
|
|
FROM tasks_assignees ta
|
|
WHERE pm.team_member_id = ta.team_member_id
|
|
AND task_id IN (SELECT id FROM tasks t WHERE t.project_id = pm.project_id))::INT AS task_count,
|
|
(SELECT COUNT(*)
|
|
FROM tasks
|
|
WHERE archived IS FALSE
|
|
AND project_id = pm.project_id
|
|
AND CASE
|
|
WHEN (TRUE IS TRUE) THEN project_id IS NOT NULL
|
|
ELSE archived IS FALSE END)::INT AS project_task_count,
|
|
(SELECT COUNT(*)
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON ta.task_id = t.id
|
|
WHERE project_id = pm.project_id
|
|
AND ta.team_member_id = pm.team_member_id
|
|
AND is_completed(t.status_id, t.project_id) IS TRUE)::INT AS completed,
|
|
(SELECT COUNT(*)
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON ta.task_id = t.id
|
|
WHERE project_id = pm.project_id
|
|
AND ta.team_member_id = pm.team_member_id
|
|
AND is_overdue(t.status_id) IS TRUE)::INT AS overdue,
|
|
(SELECT COUNT(*)
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON ta.task_id = t.id
|
|
WHERE project_id = pm.project_id
|
|
AND ta.team_member_id = pm.team_member_id
|
|
AND is_completed(t.status_id, t.project_id) IS FALSE)::INT AS incompleted,
|
|
(SELECT SUM(time_spent)
|
|
FROM task_work_log twl
|
|
WHERE task_id IN (SELECT id FROM tasks WHERE tasks.project_id = pm.project_id)
|
|
AND user_id = (SELECT user_id FROM team_member_info_view tmiv WHERE pm.team_member_id = tmiv.team_member_id)) AS time_logged
|
|
FROM project_members pm
|
|
LEFT JOIN projects p ON p.id = pm.project_id
|
|
WHERE ${teamClause} ${searchQuery} ${archivedClause}
|
|
ORDER BY name;`;
|
|
const result = await db.query(q, []);
|
|
|
|
for (const project of result.rows) {
|
|
project.time_logged = formatDuration(moment.duration(project.time_logged, "seconds"));
|
|
project.contribution = project.project_task_count > 0 ? ((project.task_count / project.project_task_count) * 100).toFixed(0) : 0;
|
|
}
|
|
|
|
return result.rows;
|
|
}
|
|
|
|
@HandleExceptions()
|
|
public static async getMemberProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
|
const { searchQuery } = this.toPaginationOptions(req.query, ["p.name"]);
|
|
const { teamMemberId, teamId } = req.query;
|
|
const archived = req.query.archived === "true";
|
|
|
|
const result = await this.getMemberProjectsData(teamId as string, teamMemberId as string, searchQuery, archived, req.user?.id as string);
|
|
|
|
return res.status(200).send(new ServerResponse(true, result));
|
|
}
|
|
|
|
|
|
protected static getMinMaxDates(key: string, dateRange: string[], tableName: string) {
|
|
if (dateRange.length === 2) {
|
|
const start = moment(dateRange[0]).format("YYYY-MM-DD");
|
|
const end = moment(dateRange[1]).format("YYYY-MM-DD");
|
|
return `,(SELECT '${start}'::DATE )AS start_date, (SELECT '${end}'::DATE )AS end_date`;
|
|
}
|
|
|
|
if (key === DATE_RANGES.YESTERDAY)
|
|
return `,(SELECT (CURRENT_DATE - INTERVAL '1 day')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date`;
|
|
if (key === DATE_RANGES.LAST_WEEK)
|
|
return `,(SELECT (CURRENT_DATE - INTERVAL '1 week')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date`;
|
|
if (key === DATE_RANGES.LAST_MONTH)
|
|
return `,(SELECT (CURRENT_DATE - INTERVAL '1 month')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date`;
|
|
if (key === DATE_RANGES.LAST_QUARTER)
|
|
return `,(SELECT (CURRENT_DATE - INTERVAL '3 months')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date`;
|
|
if (key === DATE_RANGES.ALL_TIME)
|
|
return `,(SELECT (MIN(created_at)::DATE) FROM ${tableName} WHERE task_id IN (SELECT id FROM tasks WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1))) AS start_date, (SELECT (MAX(created_at)::DATE) FROM ${tableName} WHERE task_id IN (SELECT id FROM tasks WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1))) AS end_date`;
|
|
|
|
return "";
|
|
}
|
|
|
|
|
|
|
|
@HandleExceptions()
|
|
public static async getMemberActivities(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
|
const { team_member_id, team_id, duration, date_range, archived } = req.body;
|
|
|
|
const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration || DATE_RANGES.LAST_WEEK, date_range, "tal");
|
|
const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range, "task_activity_logs");
|
|
|
|
const logGroups = await this.memberActivityLogsData(durationClause, minMaxDateClause, team_id, team_member_id, archived, req.user?.id as string);
|
|
|
|
return res.status(200).send(new ServerResponse(true, logGroups));
|
|
}
|
|
|
|
private static async formatLog(result: { start_date: string, end_date: string, time_logs: any[] }) {
|
|
|
|
result.time_logs.forEach((row) => {
|
|
const duration = moment.duration(row.time_spent, "seconds");
|
|
row.time_spent_string = this.formatDuration(duration);
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
private static async getTimeLogDays(result: { start_date: string, end_date: string, time_logs: any[] }) {
|
|
if (result) {
|
|
const startDate = moment(result.start_date).isValid() ? moment(result.start_date, "YYYY-MM-DD").clone() : null;
|
|
const endDate = moment(result.end_date).isValid() ? moment(result.end_date, "YYYY-MM-DD").clone() : null;
|
|
|
|
const days = [];
|
|
const logDayGroups = [];
|
|
|
|
while (startDate && moment(startDate).isSameOrBefore(endDate)) {
|
|
days.push(startDate.clone().format("YYYY-MM-DD"));
|
|
startDate ? startDate.add(1, "day") : null;
|
|
}
|
|
|
|
for (const day of days) {
|
|
const logsForDay = result.time_logs.filter((log) => moment(moment(log.created_at).format("YYYY-MM-DD")).isSame(moment(day).format("YYYY-MM-DD")));
|
|
if (logsForDay.length) {
|
|
logDayGroups.push({
|
|
log_day: day,
|
|
logs: logsForDay
|
|
});
|
|
}
|
|
}
|
|
|
|
return logDayGroups;
|
|
|
|
}
|
|
return [];
|
|
}
|
|
|
|
private static async getActivityLogDays(result: { start_date: string, end_date: string, activity_logs: any[] }) {
|
|
if (result) {
|
|
const startDate = moment(result.start_date).isValid() ? moment(result.start_date, "YYYY-MM-DD").clone() : null;
|
|
const endDate = moment(result.end_date).isValid() ? moment(result.end_date, "YYYY-MM-DD").clone() : null;
|
|
|
|
const days = [];
|
|
const logDayGroups = [];
|
|
|
|
while (startDate && moment(startDate).isSameOrBefore(endDate)) {
|
|
days.push(startDate.clone().format("YYYY-MM-DD"));
|
|
startDate ? startDate.add(1, "day") : null;
|
|
}
|
|
|
|
for (const day of days) {
|
|
const logsForDay = result.activity_logs.filter((log) => moment(moment(log.created_at).format("YYYY-MM-DD")).isSame(moment(day).format("YYYY-MM-DD")));
|
|
if (logsForDay.length) {
|
|
logDayGroups.push({
|
|
log_day: day,
|
|
logs: logsForDay
|
|
});
|
|
}
|
|
}
|
|
|
|
return logDayGroups;
|
|
|
|
}
|
|
return [];
|
|
}
|
|
|
|
|
|
private static async memberTimeLogsData(durationClause: string, minMaxDateClause: string, team_id: string, team_member_id: string, includeArchived: boolean, userId: string, billableQuery = "") {
|
|
|
|
const archivedClause = includeArchived
|
|
? ""
|
|
: `AND project_id NOT IN (SELECT project_id FROM archived_projects WHERE archived_projects.user_id = '${userId}')`;
|
|
|
|
const q = `
|
|
SELECT user_id,
|
|
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(tl))), '[]'::JSON)
|
|
FROM (SELECT time_spent,
|
|
created_at,
|
|
twl.task_id AS task_id,
|
|
(SELECT project_id FROM tasks WHERE tasks.id = twl.task_id) AS project_id,
|
|
(SELECT name FROM projects WHERE id = (SELECT project_id FROM tasks WHERE tasks.id = twl.task_id)) AS project_name,
|
|
(SELECT name FROM tasks WHERE tasks.id = twl.task_id) AS task_name,
|
|
CONCAT((SELECT key
|
|
FROM projects
|
|
WHERE id = (SELECT project_id FROM tasks WHERE tasks.id = twl.task_id)), '-',
|
|
(SELECT task_no FROM tasks WHERE tasks.id = twl.task_id)) AS task_key
|
|
FROM task_work_log twl
|
|
WHERE twl.user_id = tmiv.user_id
|
|
${durationClause}
|
|
AND task_id IN (SELECT id FROM tasks WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1) ${archivedClause} ${billableQuery})
|
|
ORDER BY twl.updated_at DESC) tl) AS time_logs
|
|
${minMaxDateClause}
|
|
FROM team_member_info_view tmiv
|
|
WHERE tmiv.team_id = $1
|
|
AND tmiv.team_member_id = $2
|
|
`;
|
|
|
|
const result = await db.query(q, [team_id, team_member_id]);
|
|
|
|
let logGroups: any[] = [];
|
|
|
|
if (result.rows.length) {
|
|
const [data] = result.rows;
|
|
|
|
const formattedLogs = await this.formatLog(data);
|
|
|
|
logGroups = await this.getTimeLogDays(formattedLogs);
|
|
}
|
|
|
|
return logGroups;
|
|
}
|
|
|
|
private static async memberActivityLogsData(durationClause: string, minMaxDateClause: string, team_id: string, team_member_id: string, includeArchived:boolean, userId: string) {
|
|
|
|
const archivedClause = includeArchived ? `` : `AND (SELECT project_id FROM tasks WHERE id = tal.task_id) NOT IN (SELECT project_id FROM archived_projects WHERE archived_projects.user_id = '${userId}')`;
|
|
|
|
const q = `
|
|
SELECT user_id,
|
|
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(al))), '[]'::JSON)
|
|
FROM (SELECT task_id,
|
|
user_id,
|
|
(SELECT project_id FROM tasks WHERE id = tal.task_id) AS project_id,
|
|
tal.task_id AS task_id,
|
|
(SELECT name FROM projects WHERE id = (SELECT project_id FROM tasks WHERE id = tal.task_id)) AS project_name,
|
|
(SELECT name FROM tasks WHERE tasks.id = tal.task_id) AS task_name,
|
|
CONCAT((SELECT key
|
|
FROM projects
|
|
WHERE id = (SELECT project_id FROM tasks WHERE tasks.id = tal.task_id)), '-',
|
|
(SELECT task_no FROM tasks WHERE tasks.id = tal.task_id)) AS task_key,
|
|
created_at,
|
|
attribute_type,
|
|
log_type,
|
|
(CASE
|
|
WHEN (attribute_type = 'status' AND old_value <> 'Unmapped')
|
|
THEN (SELECT name FROM task_statuses WHERE id = old_value::UUID)
|
|
WHEN (attribute_type = 'priority' AND old_value <> 'Unmapped')
|
|
THEN (SELECT name FROM task_priorities WHERE id = old_value::UUID)
|
|
WHEN (attribute_type = 'phase' AND old_value <> 'Unmapped')
|
|
THEN (SELECT name FROM project_phases WHERE id = old_value::UUID)
|
|
ELSE (old_value) END) AS previous,
|
|
|
|
(CASE
|
|
WHEN (attribute_type = 'status' AND new_value <> 'Unmapped')
|
|
THEN (SELECT name FROM task_statuses WHERE id = new_value::UUID)
|
|
WHEN (attribute_type = 'priority' AND new_value <> 'Unmapped')
|
|
THEN (SELECT name FROM task_priorities WHERE id = new_value::UUID)
|
|
WHEN (attribute_type = 'phase' AND new_value <> 'Unmapped')
|
|
THEN (SELECT name FROM project_phases WHERE id = new_value::UUID)
|
|
ELSE (new_value) END) AS current,
|
|
|
|
(CASE
|
|
WHEN (attribute_type = 'status' AND old_value <> 'Unmapped')
|
|
THEN (SELECT ROW_TO_JSON(rec)
|
|
FROM (SELECT (SELECT name FROM task_statuses WHERE id = old_value::UUID),
|
|
(SELECT color_code
|
|
FROM sys_task_status_categories
|
|
WHERE id = (SELECT category_id FROM task_statuses WHERE id = old_value::UUID))) rec)
|
|
ELSE (NULL) END) AS previous_status,
|
|
|
|
(CASE
|
|
WHEN (attribute_type = 'status' AND new_value <> 'Unmapped')
|
|
THEN (SELECT ROW_TO_JSON(rec)
|
|
FROM (SELECT (SELECT name FROM task_statuses WHERE id = new_value::UUID),
|
|
(SELECT color_code
|
|
FROM sys_task_status_categories
|
|
WHERE id = (SELECT category_id FROM task_statuses WHERE id = new_value::UUID))) rec)
|
|
ELSE (NULL) END) AS next_status,
|
|
|
|
(CASE
|
|
WHEN (attribute_type = 'priority' AND old_value <> 'Unmapped')
|
|
THEN (SELECT ROW_TO_JSON(rec)
|
|
FROM (SELECT (SELECT name FROM task_priorities WHERE id = old_value::UUID),
|
|
(SELECT color_code FROM task_priorities WHERE id = old_value::UUID)) rec)
|
|
ELSE (NULL) END) AS previous_priority,
|
|
|
|
(CASE
|
|
WHEN (attribute_type = 'priority' AND new_value <> 'Unmapped')
|
|
THEN (SELECT ROW_TO_JSON(rec)
|
|
FROM (SELECT (SELECT name FROM task_priorities WHERE id = new_value::UUID),
|
|
(SELECT color_code FROM task_priorities WHERE id = new_value::UUID)) rec)
|
|
ELSE (NULL) END) AS next_priority,
|
|
|
|
(CASE
|
|
WHEN (attribute_type = 'phase' AND old_value <> 'Unmapped')
|
|
THEN (SELECT ROW_TO_JSON(rec)
|
|
FROM (SELECT (SELECT name FROM project_phases WHERE id = old_value::UUID),
|
|
(SELECT color_code FROM project_phases WHERE id = old_value::UUID)) rec)
|
|
ELSE (NULL) END) AS previous_phase,
|
|
|
|
(CASE
|
|
WHEN (attribute_type = 'phase' AND new_value <> 'Unmapped')
|
|
THEN (SELECT ROW_TO_JSON(rec)
|
|
FROM (SELECT (SELECT name FROM project_phases WHERE id = new_value::UUID),
|
|
(SELECT color_code FROM project_phases WHERE id = new_value::UUID)) rec)
|
|
ELSE (NULL) END) AS next_phase
|
|
|
|
FROM task_activity_logs tal
|
|
WHERE tal.user_id = tmiv.user_id
|
|
${durationClause}
|
|
AND tal.team_id = $1 AND tal.attribute_type IN ('status', 'priority', 'phase', 'end_date', 'start_date')
|
|
${archivedClause}
|
|
ORDER BY created_at DESC) al) AS activity_logs
|
|
${minMaxDateClause}
|
|
FROM team_member_info_view tmiv
|
|
WHERE tmiv.team_id = $1
|
|
AND tmiv.team_member_id = $2
|
|
`;
|
|
|
|
const result = await db.query(q, [team_id, team_member_id]);
|
|
|
|
let logGroups: any[] = [];
|
|
|
|
if (result.rows.length) {
|
|
const [data] = result.rows;
|
|
|
|
logGroups = await this.getActivityLogDays(data);
|
|
}
|
|
|
|
return logGroups;
|
|
|
|
}
|
|
|
|
protected static buildBillableQuery(selectedStatuses: { billable: boolean; nonBillable: boolean }): string {
|
|
const { billable, nonBillable } = selectedStatuses;
|
|
|
|
if (billable && nonBillable) {
|
|
// Both are enabled, no need to filter
|
|
return "";
|
|
} else if (billable) {
|
|
// Only billable is enabled
|
|
return " AND tasks.billable IS TRUE";
|
|
} else if (nonBillable) {
|
|
// Only non-billable is enabled
|
|
return " AND tasks.billable IS FALSE";
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
@HandleExceptions()
|
|
public static async getMemberTimelogs(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
|
const { team_member_id, team_id, duration, date_range, archived, billable } = req.body;
|
|
|
|
// Get user timezone for proper date filtering
|
|
const userTimezone = await this.getUserTimezone(req.user?.id as string);
|
|
const durationClause = this.getDateRangeClauseWithTimezone(duration || DATE_RANGES.LAST_WEEK, date_range, userTimezone);
|
|
const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range, "task_work_log");
|
|
|
|
const billableQuery = this.buildBillableQuery(billable);
|
|
|
|
const logGroups = await this.memberTimeLogsData(durationClause, minMaxDateClause, team_id, team_member_id, archived, req.user?.id as string, billableQuery);
|
|
|
|
return res.status(200).send(new ServerResponse(true, logGroups));
|
|
}
|
|
|
|
@HandleExceptions()
|
|
public static async getMemberTaskStats(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
|
|
|
const { duration, date_range, team_member_id } = req.query;
|
|
const includeArchived = req.query.archived === "true";
|
|
|
|
let dateRange: string[] = [];
|
|
if (typeof date_range === "string") {
|
|
dateRange = date_range.split(",");
|
|
}
|
|
|
|
const archivedClause = includeArchived
|
|
? ""
|
|
: `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${req.user?.id}')`;
|
|
|
|
|
|
const assignClause = this.memberAssignDurationFilter(duration as string, dateRange);
|
|
const completedDurationClasue = this.completedDurationFilter(duration as string, dateRange);
|
|
const overdueClauseByDate = this.getActivityLogsOverdue(duration as string, dateRange);
|
|
const taskSelectorClause = this.getTaskSelectorClause();
|
|
const durationFilter = this.memberTasksDurationFilter(duration as string, dateRange);
|
|
|
|
const q = `
|
|
SELECT name AS team_member_name,
|
|
|
|
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(assigned))), '[]')
|
|
FROM (${taskSelectorClause}
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
|
WHERE ta.team_member_id = $1 ${assignClause} ${archivedClause}) assigned) AS assigned,
|
|
|
|
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(assigned))), '[]')
|
|
FROM (${taskSelectorClause}
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
|
WHERE ta.team_member_id = $1 ${durationFilter} ${assignClause} ${archivedClause}) assigned) AS total,
|
|
|
|
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(completed))), '[]')
|
|
FROM (${taskSelectorClause}
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
|
WHERE ta.team_member_id = $1
|
|
AND is_completed(status_id, t.project_id)
|
|
${completedDurationClasue} ${archivedClause}) completed) AS completed,
|
|
|
|
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(ongoing))), '[]')
|
|
FROM (${taskSelectorClause}
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
|
WHERE ta.team_member_id = $1
|
|
AND is_doing(status_id, t.project_id) ${archivedClause}) ongoing) AS ongoing,
|
|
|
|
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(overdue))), '[]')
|
|
FROM (${taskSelectorClause}
|
|
FROM tasks t
|
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
|
WHERE ta.team_member_id = $1 ${overdueClauseByDate} ${archivedClause}) overdue) AS overdue
|
|
|
|
FROM team_member_info_view WHERE team_member_id = $1;
|
|
`;
|
|
|
|
const result = await db.query(q, [team_member_id]);
|
|
const [data] = result.rows;
|
|
|
|
if (data) {
|
|
for (const taskArray of [data.assigned, data.completed, data.ongoing, data.overdue]) {
|
|
this.updateTaskProperties(taskArray);
|
|
}
|
|
}
|
|
|
|
const body = {
|
|
team_member_name: data.team_member_name,
|
|
groups: [
|
|
{
|
|
name: "Total Tasks",
|
|
color_code: "#7590c9",
|
|
tasks: data.total ? data.total : 0
|
|
},
|
|
{
|
|
name: "Tasks Assigned",
|
|
color_code: "#7590c9",
|
|
tasks: data.assigned ? data.assigned : 0
|
|
},
|
|
{
|
|
name: "Tasks Completed",
|
|
color_code: "#75c997",
|
|
tasks: data.completed ? data.completed : 0
|
|
},
|
|
{
|
|
name: "Tasks Overdue",
|
|
color_code: "#eb6363",
|
|
tasks: data.overdue ? data.overdue : 0
|
|
},
|
|
{
|
|
name: "Tasks Ongoing",
|
|
color_code: "#7cb5ec",
|
|
tasks: data.ongoing ? data.ongoing : 0
|
|
},
|
|
]
|
|
};
|
|
|
|
return res.status(200).send(new ServerResponse(true, body));
|
|
}
|
|
|
|
private static updateTaskProperties(tasks: any[]) {
|
|
for (const task of tasks) {
|
|
task.project_color = getColor(task.project_name);
|
|
task.estimated_string = formatDuration(moment.duration(~~(task.total_minutes), "seconds"));
|
|
task.time_spent_string = formatDuration(moment.duration(~~(task.time_logged), "seconds"));
|
|
task.overlogged_time_string = formatDuration(moment.duration(~~(task.overlogged_time), "seconds"));
|
|
task.overdue_days = task.days_overdue ? task.days_overdue : null;
|
|
}
|
|
}
|
|
|
|
@HandleExceptions()
|
|
public static async getSingleMemberProjects(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
|
const { team_member_id } = req.query;
|
|
const includeArchived = req.query.archived === "true";
|
|
|
|
const archivedClause = includeArchived
|
|
? ""
|
|
: `AND projects.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = projects.id AND archived_projects.user_id = '${req.user?.id}')`;
|
|
|
|
const q = `SELECT id,
|
|
name,
|
|
color_code,
|
|
start_date,
|
|
end_date,
|
|
|
|
(SELECT icon FROM sys_project_statuses WHERE id = projects.status_id) AS status_icon,
|
|
(SELECT name FROM sys_project_statuses WHERE id = projects.status_id) AS status_name,
|
|
(SELECT color_code FROM sys_project_statuses WHERE id = projects.status_id) AS status_color,
|
|
|
|
(SELECT name FROM sys_project_healths WHERE id = projects.health_id) AS health_name,
|
|
(SELECT color_code FROM sys_project_healths WHERE id = projects.health_id) AS health_color,
|
|
|
|
(SELECT name FROM project_categories WHERE id = projects.category_id) AS category_name,
|
|
(SELECT color_code
|
|
FROM project_categories
|
|
WHERE id = projects.category_id) AS category_color,
|
|
|
|
(SELECT COALESCE(ROW_TO_JSON(pm), '{}'::JSON)
|
|
FROM (SELECT team_member_id AS pm_id,
|
|
(SELECT COALESCE(ROW_TO_JSON(pmi), '{}'::JSON)
|
|
FROM (SELECT name, email, avatar_url
|
|
FROM team_member_info_view tmiv
|
|
WHERE tmiv.team_member_id = pm.team_member_id) pmi) AS project_manager_info,
|
|
EXISTS(SELECT email
|
|
FROM email_invitations
|
|
WHERE team_member_id = pm.team_member_id
|
|
AND email_invitations.team_id = (SELECT team_id
|
|
FROM team_member_info_view
|
|
WHERE team_member_id = pm.team_member_id)) AS pending_invitation,
|
|
(SELECT active FROM team_members WHERE id = pm.team_member_id)
|
|
FROM project_members pm
|
|
WHERE project_id = projects.id
|
|
AND project_access_level_id =
|
|
(SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')) pm) AS project_manager,
|
|
|
|
(SELECT COALESCE(SUM(total_minutes), 0)
|
|
FROM tasks
|
|
WHERE project_id = projects.id) AS estimated_time,
|
|
|
|
(SELECT SUM((SELECT COALESCE(SUM(time_spent), 0)
|
|
FROM task_work_log
|
|
WHERE task_id = tasks.id))
|
|
FROM tasks
|
|
WHERE project_id = projects.id) AS actual_time,
|
|
|
|
(SELECT name FROM team_member_info_view WHERE team_member_id = $1) As team_member_name
|
|
|
|
FROM projects
|
|
WHERE projects.id IN (SELECT project_id FROM project_members WHERE team_member_id = $1) ${archivedClause};`;
|
|
|
|
const result = await db.query(q, [team_member_id]);
|
|
const data = result.rows;
|
|
|
|
for (const row of data) {
|
|
row.estimated_time = int(row.estimated_time);
|
|
row.actual_time = int(row.actual_time);
|
|
row.estimated_time_string = this.convertMinutesToHoursAndMinutes(int(row.estimated_time));
|
|
row.actual_time_string = this.convertSecondsToHoursAndMinutes(int(row.actual_time));
|
|
row.days_left = this.getDaysLeft(row.end_date);
|
|
row.is_overdue = this.isOverdue(row.end_date);
|
|
if (row.days_left && row.is_overdue) {
|
|
row.days_left = row.days_left.toString().replace(/-/g, "");
|
|
}
|
|
row.is_today = this.isToday(row.end_date);
|
|
if (row.project_manager) {
|
|
row.project_manager.name = row.project_manager.project_manager_info.name;
|
|
row.project_manager.avatar_url = row.project_manager.project_manager_info.avatar_url;
|
|
row.project_manager.color_code = getColor(row.project_manager.name);
|
|
}
|
|
row.project_health = row.health_name ? row.health_name : null;
|
|
}
|
|
|
|
const body = {
|
|
team_member_name: data[0].team_member_name,
|
|
projects: data
|
|
};
|
|
|
|
return res.status(200).send(new ServerResponse(true, body));
|
|
|
|
}
|
|
|
|
@HandleExceptions()
|
|
public static async exportMemberProjects(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
|
const teamMemberId = (req.query.team_member_id as string)?.trim() || null;
|
|
const teamId = (req.query.team_id as string)?.trim() || null;
|
|
const memberName = (req.query.team_member_name as string)?.trim() || null;
|
|
const teamName = (req.query.team_name as string)?.trim() || "";
|
|
const archived = req.query.archived === "true";
|
|
|
|
const result = await this.getMemberProjectsData(teamId as string, teamMemberId as string, "", archived, req.user?.id as string);
|
|
|
|
// excel file
|
|
const exportDate = moment().format("MMM-DD-YYYY");
|
|
const fileName = `${memberName} projects - ${exportDate}`;
|
|
const workbook = new Excel.Workbook();
|
|
|
|
const sheet = workbook.addWorksheet("Projects");
|
|
|
|
sheet.columns = [
|
|
{ header: "Project", key: "project", width: 30 },
|
|
{ header: "Team", key: "team", width: 20 },
|
|
{ header: "Tasks", key: "tasks", width: 20 },
|
|
{ header: "Contribution(%)", key: "contribution", width: 20 },
|
|
{ header: "Incompleted Tasks", key: "incompleted_tasks", width: 20 },
|
|
{ header: "Completed Tasks", key: "completed_tasks", width: 20 },
|
|
{ header: "Overdue Tasks", key: "overdue_tasks", width: 20 },
|
|
{ header: "Logged Time", key: "logged_time", width: 20 }
|
|
];
|
|
|
|
// set title
|
|
sheet.getCell("A1").value = `Projects of ${memberName} - ${teamName}`;
|
|
sheet.mergeCells("A1:H1");
|
|
sheet.getCell("A1").alignment = { horizontal: "center" };
|
|
sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
|
|
sheet.getCell("A1").font = { size: 16 };
|
|
|
|
// set export date
|
|
sheet.getCell("A2").value = `Exported on : ${exportDate}`;
|
|
sheet.mergeCells("A2:H2");
|
|
sheet.getCell("A2").alignment = { horizontal: "center" };
|
|
sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
|
|
sheet.getCell("A2").font = { size: 12 };
|
|
|
|
// set duration
|
|
// const start = 'duartion start';
|
|
// const end = 'duartion end';
|
|
// sheet.getCell("A3").value = `From : ${start} To : ${end}`;
|
|
// sheet.mergeCells("A3:D3");
|
|
|
|
// set table headers
|
|
sheet.getRow(4).values = ["Project", "Team", "Tasks", "Contribution(%)", "Incompleted Tasks", "Completed Tasks", "Overdue Tasks", "Logged Time"];
|
|
sheet.getRow(4).font = { bold: true };
|
|
|
|
for (const project of result) {
|
|
sheet.addRow({
|
|
project: project.name,
|
|
team: project.team,
|
|
tasks: project.task_count ? project.task_count.toString() : "-",
|
|
contribution: project.contribution ? project.contribution.toString() : "-",
|
|
incompleted_tasks: project.incompleted ? project.incompleted.toString() : "-",
|
|
completed_tasks: project.completed ? project.completed.toString() : "-",
|
|
overdue_tasks: project.overdue ? project.overdue.toString() : "-",
|
|
logged_time: project.time_logged ? project.time_logged.toString() : "-"
|
|
});
|
|
}
|
|
|
|
// download excel
|
|
res.setHeader("Content-Type", "application/vnd.openxmlformats");
|
|
res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
|
|
|
|
await workbook.xlsx.write(res)
|
|
.then(() => {
|
|
res.end();
|
|
});
|
|
|
|
}
|
|
|
|
}
|