Initial commit: Angular frontend and Expressjs backend

This commit is contained in:
chamikaJ
2024-05-17 09:32:30 +05:30
parent eb0a0d77d6
commit 298ca6beeb
3548 changed files with 193558 additions and 3 deletions

View File

@@ -0,0 +1,60 @@
import { IChartObject } from "./overview/reporting-overview-base";
export interface IDuration {
label: string;
key: string;
}
export interface IReportingInfo {
organization_name: string;
}
export interface ITeamStatistics {
count: number;
projects: number;
members: number;
}
export interface IProjectStatistics {
count: number;
active: number;
overdue: number;
}
export interface IMemberStatistics {
count: number;
unassigned: number;
overdue: number;
}
export interface IOverviewStatistics {
teams: ITeamStatistics;
projects: IProjectStatistics;
members: IMemberStatistics;
}
export interface IChartData {
chart: IChartObject[];
}
export interface ITasksByStatus extends IChartData {
all: number;
todo: number;
doing: number;
done: number;
}
export interface ITasksByPriority extends IChartData {
all: number;
low: number;
medium: number;
high: number;
}
export interface ITasksByDue extends IChartData {
all: number;
completed: number;
upcoming: number;
overdue: number;
no_due: number;
}

View File

@@ -0,0 +1,993 @@
import db from "../../../config/db";
import { ITasksByDue, ITasksByPriority, ITasksByStatus } from "../interfaces";
import ReportingControllerBase from "../reporting-controller-base";
import {
DATE_RANGES,
TASK_DUE_COMPLETED_COLOR,
TASK_DUE_NO_DUE_COLOR,
TASK_DUE_OVERDUE_COLOR,
TASK_DUE_UPCOMING_COLOR,
TASK_PRIORITY_HIGH_COLOR,
TASK_PRIORITY_LOW_COLOR,
TASK_PRIORITY_MEDIUM_COLOR,
TASK_STATUS_DOING_COLOR,
TASK_STATUS_DONE_COLOR,
TASK_STATUS_TODO_COLOR
} from "../../../shared/constants";
import { formatDuration, int } from "../../../shared/utils";
import moment from "moment";
export interface IChartObject {
name: string,
color: string,
y: number
}
export default class ReportingOverviewBase extends ReportingControllerBase {
private static createChartObject(name: string, color: string, y: number) {
return {
name,
color,
y
};
}
protected static async getTeamsCounts(teamId: string | null, archivedQuery = "") {
const q = `
SELECT JSON_BUILD_OBJECT(
'teams', (SELECT COUNT(*) FROM teams WHERE in_organization(id, $1)),
'projects',
(SELECT COUNT(*) FROM projects WHERE in_organization(team_id, $1) ${archivedQuery}),
'team_members', (SELECT COUNT(DISTINCT email)
FROM team_member_info_view
WHERE in_organization(team_id, $1))
) AS counts;
`;
const res = await db.query(q, [teamId]);
const [data] = res.rows;
return {
count: int(data?.counts.teams),
projects: int(data?.counts.projects),
members: int(data?.counts.team_members),
};
}
protected static async getProjectsCounts(teamId: string | null, archivedQuery = "") {
const q = `
SELECT JSON_BUILD_OBJECT(
'active_projects', (SELECT COUNT(*)
FROM projects
WHERE in_organization(team_id, $1) AND (end_date > CURRENT_TIMESTAMP
OR end_date IS NULL) ${archivedQuery}),
'overdue_projects', (SELECT COUNT(*)
FROM projects
WHERE in_organization(team_id, $1)
AND end_date < CURRENT_TIMESTAMP
AND status_id NOT IN
(SELECT id FROM sys_project_statuses WHERE name = 'Completed') ${archivedQuery})
) AS counts;
`;
const res = await db.query(q, [teamId]);
const [data] = res.rows;
return {
count: 0,
active: int(data?.counts.active_projects),
overdue: int(data?.counts.overdue_projects),
};
}
protected static async getMemberCounts(teamId: string | null) {
const q = `
SELECT JSON_BUILD_OBJECT(
'unassigned', (SELECT COUNT(*)
FROM team_members
WHERE in_organization(team_id, $1)
AND id NOT IN (SELECT team_member_id FROM tasks_assignees)),
'with_overdue', (SELECT COUNT(*)
FROM team_members
WHERE in_organization(team_id, $1)
AND id IN (SELECT team_member_id
FROM tasks_assignees
WHERE is_overdue(task_id) IS TRUE))
) AS counts;
`;
const res = await db.query(q, [teamId]);
const [data] = res.rows;
return {
count: 0,
unassigned: int(data?.counts.unassigned),
overdue: int(data?.counts.with_overdue),
};
}
protected static async getProjectStats(projectId: string | null) {
const q = `
SELECT JSON_BUILD_OBJECT(
'completed', (SELECT COUNT(*)
FROM tasks
WHERE project_id = $1
AND is_completed(tasks.status_id, tasks.project_id) IS TRUE),
'incompleted', (SELECT COUNT(*)
FROM tasks
WHERE project_id = $1
AND is_completed(tasks.status_id, tasks.project_id) IS FALSE),
'overdue', (SELECT COUNT(*)
FROM tasks
WHERE project_id = $1
AND is_overdue(tasks.id)),
'total_allocated', (SELECT SUM(total_minutes)
FROM tasks
WHERE project_id = $1),
'total_logged', (SELECT SUM((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = tasks.id))
FROM tasks
WHERE project_id = $1)
) AS counts;
`;
const res = await db.query(q, [projectId]);
const [data] = res.rows;
return {
completed: int(data?.counts.completed),
incompleted: int(data?.counts.incompleted),
overdue: int(data?.counts.overdue),
total_allocated: moment.duration(int(data?.counts.total_allocated), "minutes").asHours().toFixed(0),
total_logged: moment.duration(int(data?.counts.total_logged), "seconds").asHours().toFixed(0),
};
}
protected static async getTasksByStatus(projectId: string | null): Promise<ITasksByStatus> {
const q = `
SELECT JSON_BUILD_OBJECT(
'all', (SELECT COUNT(*)
FROM tasks
WHERE project_id = $1),
'todo', (SELECT COUNT(*)
FROM tasks
WHERE project_id = $1
AND is_todo(tasks.status_id, tasks.project_id) IS TRUE),
'doing', (SELECT COUNT(*)
FROM tasks
WHERE project_id = $1
AND is_doing(tasks.status_id, tasks.project_id) IS TRUE),
'done', (SELECT COUNT(*)
FROM tasks
WHERE project_id = $1
AND is_completed(tasks.status_id, tasks.project_id) IS TRUE)
) AS counts;
`;
const res = await db.query(q, [projectId]);
const [data] = res.rows;
const all = int(data?.counts.all);
const todo = int(data?.counts.todo);
const doing = int(data?.counts.doing);
const done = int(data?.counts.done);
const chart: IChartObject[] = [];
return {
all,
todo,
doing,
done,
chart
};
}
protected static async getTasksByPriority(projectId: string | null): Promise<ITasksByPriority> {
const q = `
SELECT JSON_BUILD_OBJECT(
'low', (SELECT COUNT(*)
FROM tasks
WHERE project_id = $1
AND priority_id = (SELECT id FROM task_priorities WHERE value = 0)),
'medium', (SELECT COUNT(*)
FROM tasks
WHERE project_id = $1
AND priority_id = (SELECT id FROM task_priorities WHERE value = 1)),
'high', (SELECT COUNT(*)
FROM tasks
WHERE project_id = $1
AND priority_id = (SELECT id FROM task_priorities WHERE value = 2))
) AS counts;
`;
const res = await db.query(q, [projectId]);
const [data] = res.rows;
const low = int(data?.counts.low);
const medium = int(data?.counts.medium);
const high = int(data?.counts.high);
const chart: IChartObject[] = [];
return {
all: 0,
low,
medium,
high,
chart
};
}
protected static async getTaskCountsByDue(projectId: string | null): Promise<ITasksByDue> {
const q = `
SELECT JSON_BUILD_OBJECT(
'no_due', (SELECT COUNT(*)
FROM tasks
WHERE project_id = $1
AND end_date IS NULL),
'upcoming', (SELECT COUNT(*)
FROM tasks
WHERE project_id = $1
AND end_date > CURRENT_TIMESTAMP)
) AS counts;
`;
const res = await db.query(q, [projectId]);
const [data] = res.rows;
const chart: IChartObject[] = [];
return {
all: 0,
completed: 0,
upcoming: int(data?.counts.upcoming),
overdue: 0,
no_due: int(data?.counts.no_due),
chart
};
}
protected static createByStatusChartData(body: ITasksByStatus) {
body.chart = [
this.createChartObject("Todo", TASK_STATUS_TODO_COLOR, body.todo),
this.createChartObject("Doing", TASK_STATUS_DOING_COLOR, body.doing),
this.createChartObject("Done", TASK_STATUS_DONE_COLOR, body.done),
];
}
protected static createByPriorityChartData(body: ITasksByPriority) {
body.chart = [
this.createChartObject("Low", TASK_PRIORITY_LOW_COLOR, body.low),
this.createChartObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, body.medium),
this.createChartObject("High", TASK_PRIORITY_HIGH_COLOR, body.high),
];
}
protected static createByDueDateChartData(body: ITasksByDue) {
body.chart = [
this.createChartObject("Completed", TASK_DUE_COMPLETED_COLOR, body.completed),
this.createChartObject("Upcoming", TASK_DUE_UPCOMING_COLOR, body.upcoming),
this.createChartObject("Overdue", TASK_DUE_OVERDUE_COLOR, body.overdue),
this.createChartObject("No due date", TASK_DUE_NO_DUE_COLOR, body.no_due),
];
}
// Team Member Overview
protected static async getProjectCountOfTeamMember(teamMemberId: string | null, includeArchived: boolean, userId: string) {
const archivedClause = includeArchived
? ""
: `AND pm.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = pm.project_id AND archived_projects.user_id = '${userId}')`;
const q = `
SELECT COUNT(*)
FROM project_members pm
WHERE team_member_id = $1 ${archivedClause};
`;
const result = await db.query(q, [teamMemberId]);
const [data] = result.rows;
return int(data.count);
}
protected static async getTeamCountOfTeamMember(teamMemberId: string | null) {
const q = `
SELECT COUNT(*)
FROM team_members
WHERE id = $1;
`;
const result = await db.query(q, [teamMemberId]);
const [data] = result.rows;
return int(data.count);
}
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");
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 activityLogDurationFilter(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");
return `
AND (is_doing(
(SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' AND tl.created_at::DATE <= '${end}'::DATE ORDER BY tl.created_at DESC LIMIT 1
)::UUID, t.project_id)
OR is_todo(
(SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' AND tl.created_at::DATE <= '${end}'::DATE ORDER BY tl.created_at DESC LIMIT 1
)::UUID, t.project_id)
OR is_completed_between(t.id::UUID, '${start}'::DATE, '${end}'::DATE))`;
}
return `AND (is_doing(
(SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' AND tl.created_at::DATE <= NOW()::DATE ORDER BY tl.created_at DESC LIMIT 1
)::UUID, t.project_id)
OR is_todo(
(SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' AND tl.created_at::DATE <= NOW()::DATE ORDER BY tl.created_at DESC LIMIT 1
)::UUID, t.project_id)
OR is_completed(t.status_id::UUID, t.project_id::UUID))`;
}
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 overdueTasksByDate(key: string, dateRange: string[], archivedClause: string) {
if (dateRange.length === 2) {
const end = moment(dateRange[1]).format("YYYY-MM-DD");
return `(SELECT COUNT(CASE WHEN is_overdue_for_date(t.id, '${end}'::DATE) IS TRUE THEN 1 END)
FROM tasks t
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 ${archivedClause})`;
}
return `(SELECT COUNT(CASE WHEN is_overdue_for_date(t.id, NOW()::DATE) IS TRUE THEN 1 END)
FROM tasks t
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 ${archivedClause})`;
}
protected static overdueTasksDurationFilter(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");
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`;
if (key === DATE_RANGES.ALL_TIME)
return `AND t.end_date::DATE < NOW()::DATE`;
return "";
}
protected static taskWorklogDurationFilter(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");
return `AND created_at::DATE >= '${start}'::DATE AND created_at::DATE <= '${end}'::DATE`;
}
if (key === DATE_RANGES.YESTERDAY)
return `AND created_at::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND created_at::DATE < CURRENT_DATE::DATE`;
if (key === DATE_RANGES.LAST_WEEK)
return `AND created_at::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND created_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
if (key === DATE_RANGES.LAST_MONTH)
return `AND created_at::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND created_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
if (key === DATE_RANGES.LAST_QUARTER)
return `AND created_at::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND created_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
return "";
}
protected static async getTeamMemberStats(teamMemberId: string | null, includeArchived: boolean, userId: string) {
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 q = `SELECT JSON_BUILD_OBJECT(
'total_tasks', (SELECT COUNT(ta.task_id)
FROM tasks t
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 ${archivedClause}),
'completed', (SELECT COUNT(CASE WHEN is_completed(t.status_id, t.project_id) IS TRUE THEN 1 END)
FROM tasks t
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 ${archivedClause}),
'ongoing', (SELECT COUNT(CASE WHEN is_doing(t.status_id, t.project_id) IS TRUE THEN 1 END)
FROM tasks t
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 ${archivedClause}),
'overdue', (SELECT COUNT(CASE WHEN is_overdue(t.id) IS TRUE THEN 1 END)
FROM tasks t
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 ${archivedClause}),
'total_logged', (SELECT SUM((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id AND user_id = (SELECT user_id FROM team_member_info_view WHERE team_member_info_view.team_member_id = $1) ${archivedClause})) AS total_logged
FROM tasks t
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1)
) AS counts;`;
const res = await db.query(q, [teamMemberId]);
const [data] = res.rows;
return {
teams: 0,
projects: 0,
completed: int(data?.counts.completed),
ongoing: int(data?.counts.ongoing),
overdue: int(data?.counts.overdue),
total_tasks: int(data?.counts.total_tasks),
total_logged: formatDuration(moment.duration(data?.counts.total_logged, "seconds")),
};
}
protected static async getMemberStats(teamMemberId: string | null, key: string, dateRange: string[] | [], includeArchived: boolean, userId: string) {
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 durationFilter = this.memberTasksDurationFilter(key, dateRange);
const workLogDurationFilter = this.taskWorklogDurationFilter(key, dateRange);
const assignClause = this.memberAssignDurationFilter(key, dateRange);
const completedDurationClasue = this.completedDurationFilter(key, dateRange);
const overdueClauseByDate = this.overdueTasksByDate(key, dateRange, archivedClause);
const q = `SELECT JSON_BUILD_OBJECT(
'total_tasks', (SELECT COUNT(ta.task_id)
FROM tasks t
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 ${durationFilter} ${archivedClause}),
'assigned', (SELECT COUNT(ta.task_id)
FROM tasks t
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 ${assignClause} ${archivedClause}),
'completed', (SELECT COUNT(CASE WHEN is_completed(t.status_id, t.project_id) IS TRUE THEN 1 END)
FROM tasks t
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 ${completedDurationClasue} ${archivedClause}),
'ongoing', (SELECT COUNT(CASE WHEN is_doing(t.status_id, t.project_id) IS TRUE THEN 1 END)
FROM tasks t
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 ${archivedClause}),
'overdue', ${overdueClauseByDate},
'total_logged', (SELECT SUM((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id AND user_id = (SELECT user_id FROM team_member_info_view WHERE team_member_info_view.team_member_id = $1) ${workLogDurationFilter} ${archivedClause})) AS total_logged
FROM tasks t
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1)
) AS counts;`;
const res = await db.query(q, [teamMemberId]);
const [data] = res.rows;
return {
teams: 0,
projects: 0,
assigned: int(data?.counts.assigned),
completed: int(data?.counts.completed),
ongoing: int(data?.counts.ongoing),
overdue: int(data?.counts.overdue),
total_tasks: int(data?.counts.total_tasks),
total_logged: formatDuration(moment.duration(data?.counts.total_logged, "seconds")),
};
}
protected static async getTasksByProjectOfTeamMemberOverview(teamMemberId: string | null, includeArchived: boolean, userId: string) {
const archivedClause = includeArchived
? ""
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND archived_projects.user_id = '${userId}')`;
const q = `
SELECT p.id,
p.color_code AS color,
p.name AS label,
COUNT(t.id) AS count
FROM projects p
JOIN tasks t ON p.id = t.project_id
JOIN tasks_assignees ta ON t.id = ta.task_id AND ta.team_member_id = $1
JOIN project_members pm ON p.id = pm.project_id AND pm.team_member_id = $1
WHERE (is_doing(t.status_id, t.project_id)
OR is_todo(t.status_id, t.project_id)
OR is_completed(t.status_id, t.project_id)) ${archivedClause}
GROUP BY p.id, p.name;
`;
const result = await db.query(q, [teamMemberId]);
const chart: IChartObject[] = [];
const total = result.rows.reduce((accumulator: number, current: {
count: number
}) => accumulator + int(current.count), 0);
for (const project of result.rows) {
project.count = int(project.count);
chart.push(this.createChartObject(project.label, project.color, project.count));
}
return { chart, total, data: result.rows };
}
protected static async getTasksByProjectOfTeamMember(teamMemberId: string | null, key: string, dateRange: string[] | [], includeArchived: boolean, userId: string) {
const archivedClause = includeArchived
? ""
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND archived_projects.user_id = '${userId}')`;
const durationFilter = this.memberTasksDurationFilter(key, dateRange);
const activityLogDateFilter = this.getActivityLogsCreationClause(key, dateRange);
const completedDatebetweenClause = this.getCompletedBetweenClause(key, dateRange);
const q = `
SELECT p.id,
p.color_code AS color,
p.name AS label,
COUNT(t.id) AS count
FROM projects p
JOIN tasks t ON p.id = t.project_id
JOIN tasks_assignees ta ON t.id = ta.task_id AND ta.team_member_id = $1
JOIN project_members pm ON p.id = pm.project_id AND pm.team_member_id = $1
WHERE (is_doing(
(SELECT new_value
FROM task_activity_logs tl
WHERE tl.task_id = t.id
AND tl.attribute_type = 'status'
${activityLogDateFilter}
ORDER BY tl.created_at DESC
LIMIT 1)::UUID, t.project_id)
OR is_todo(
(SELECT new_value
FROM task_activity_logs tl
WHERE tl.task_id = t.id
AND tl.attribute_type = 'status'
${activityLogDateFilter}
ORDER BY tl.created_at DESC
LIMIT 1)::UUID, t.project_id)
OR ${completedDatebetweenClause}) ${archivedClause}
GROUP BY p.id, p.name;
`;
const result = await db.query(q, [teamMemberId]);
const chart: IChartObject[] = [];
const total = result.rows.reduce((accumulator: number, current: {
count: number
}) => accumulator + int(current.count), 0);
for (const project of result.rows) {
project.count = int(project.count);
chart.push(this.createChartObject(project.label, project.color, project.count));
}
return { chart, total, data: result.rows };
}
protected static async getTasksByPriorityOfTeamMemberOverview(teamMemberId: string | null, includeArchived: boolean, userId: string) {
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 q = `
SELECT COUNT(CASE WHEN tp.value = 0 THEN 1 END) AS low,
COUNT(CASE WHEN tp.value = 1 THEN 1 END) AS medium,
COUNT(CASE WHEN tp.value = 2 THEN 1 END) AS high
FROM tasks t
LEFT JOIN task_priorities tp ON t.priority_id = tp.id
JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 AND (is_doing(t.status_id, t.project_id)
OR is_todo(t.status_id, t.project_id)
OR is_completed(t.status_id, t.project_id)) ${archivedClause};
`;
const result = await db.query(q, [teamMemberId]);
const [d] = result.rows;
const total = int(d.low) + int(d.medium) + int(d.high);
const chart = [
this.createChartObject("Low", TASK_PRIORITY_LOW_COLOR, d.low),
this.createChartObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, d.medium),
this.createChartObject("High", TASK_PRIORITY_HIGH_COLOR, d.high),
];
const data = [
{ label: "Low", color: TASK_PRIORITY_LOW_COLOR, count: d.low },
{ label: "Medium", color: TASK_PRIORITY_MEDIUM_COLOR, count: d.medium },
{ label: "High", color: TASK_PRIORITY_HIGH_COLOR, count: d.high },
];
return { chart, total, data };
}
protected static async getTasksByPriorityOfTeamMember(teamMemberId: string | null, key: string, dateRange: string[] | [], includeArchived: boolean, userId: string) {
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 durationFilter = this.memberTasksDurationFilter(key, dateRange);
const activityLogDateFilter = this.getActivityLogsCreationClause(key, dateRange);
const completedDatebetweenClause = this.getCompletedBetweenClause(key, dateRange);
const q = `
SELECT COUNT(CASE WHEN tp.value = 0 THEN 1 END) AS low,
COUNT(CASE WHEN tp.value = 1 THEN 1 END) AS medium,
COUNT(CASE WHEN tp.value = 2 THEN 1 END) AS high
FROM tasks t
LEFT JOIN task_priorities tp ON t.priority_id = tp.id
JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 AND (is_doing(
(SELECT new_value
FROM task_activity_logs tl
WHERE tl.task_id = t.id
AND tl.attribute_type = 'status'
${activityLogDateFilter}
ORDER BY tl.created_at DESC
LIMIT 1)::UUID, t.project_id)
OR is_todo(
(SELECT new_value
FROM task_activity_logs tl
WHERE tl.task_id = t.id
AND tl.attribute_type = 'status'
${activityLogDateFilter}
ORDER BY tl.created_at DESC
LIMIT 1)::UUID, t.project_id)
OR ${completedDatebetweenClause}) ${archivedClause};
`;
const result = await db.query(q, [teamMemberId]);
const [d] = result.rows;
const total = int(d.low) + int(d.medium) + int(d.high);
const chart = [
this.createChartObject("Low", TASK_PRIORITY_LOW_COLOR, d.low),
this.createChartObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, d.medium),
this.createChartObject("High", TASK_PRIORITY_HIGH_COLOR, d.high),
];
const data = [
{ label: "Low", color: TASK_PRIORITY_LOW_COLOR, count: d.low },
{ label: "Medium", color: TASK_PRIORITY_MEDIUM_COLOR, count: d.medium },
{ label: "High", color: TASK_PRIORITY_HIGH_COLOR, count: d.high },
];
return { chart, total, data };
}
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 getCompletedBetweenClause(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");
return `is_completed_between(t.id::UUID, '${start}'::DATE, '${end}'::DATE)`;
}
return `is_completed(t.status_id::UUID, t.project_id::UUID)`;
}
protected static async getTasksByStatusOfTeamMemberOverview(teamMemberId: string | null, includeArchived: boolean, userId: string) {
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 q = `
SELECT COUNT(ta.task_id) AS total,
COUNT(CASE WHEN is_todo(t.status_id, t.project_id) IS TRUE THEN 1 END) AS todo,
COUNT(CASE WHEN is_doing(t.status_id, t.project_id) IS TRUE THEN 1 END) AS doing,
COUNT(CASE WHEN is_completed(t.status_id, t.project_id) IS TRUE THEN 1 END) AS done
FROM tasks t
JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 ${archivedClause};
`;
const res = await db.query(q, [teamMemberId]);
const [d] = res.rows;
const total = int(d.total);
const chart = [
this.createChartObject("Todo", TASK_STATUS_TODO_COLOR, d.todo),
this.createChartObject("Doing", TASK_STATUS_DOING_COLOR, d.doing),
this.createChartObject("Done", TASK_STATUS_DONE_COLOR, d.done),
];
const data = [
{ label: "Todo", color: TASK_STATUS_TODO_COLOR, count: d.todo },
{ label: "Doing", color: TASK_STATUS_DOING_COLOR, count: d.doing },
{ label: "Done", color: TASK_STATUS_DONE_COLOR, count: d.done },
];
return { chart, total, data };
}
protected static async getTasksByStatusOfTeamMember(teamMemberId: string | null, key: string, dateRange: string[] | [], includeArchived: boolean, userId: string) {
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 durationFilter = this.memberTasksDurationFilter(key, dateRange);
const completedBetweenFilter = this.getCompletedBetweenClause(key, dateRange);
const activityLogCreationFilter = this.getActivityLogsCreationClause(key, dateRange);
const q = `
SELECT COUNT(ta.task_id) AS total,
COUNT(CASE WHEN 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) IS TRUE THEN 1 END) AS todo,
COUNT(CASE WHEN 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) IS TRUE THEN 1 END) AS doing,
COUNT(CASE WHEN ${completedBetweenFilter} IS TRUE THEN 1 END) AS done
FROM tasks t
JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 ${archivedClause};
`;
const res = await db.query(q, [teamMemberId]);
const [d] = res.rows;
const total = int(d.todo) + int(d.doing) + int(d.done);
const chart = [
this.createChartObject("Todo", TASK_STATUS_TODO_COLOR, d.todo),
this.createChartObject("Doing", TASK_STATUS_DOING_COLOR, d.doing),
this.createChartObject("Done", TASK_STATUS_DONE_COLOR, d.done),
];
const data = [
{ label: "Todo", color: TASK_STATUS_TODO_COLOR, count: d.todo },
{ label: "Doing", color: TASK_STATUS_DOING_COLOR, count: d.doing },
{ label: "Done", color: TASK_STATUS_DONE_COLOR, count: d.done },
];
return { chart, total, data };
}
protected static async getProjectsByStatus(teamId: string | null, archivedClause = ""): Promise<any> {
const q = `WITH ProjectCounts AS (
SELECT
COUNT(*) AS all_projects,
SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'Cancelled') THEN 1 ELSE 0 END) AS cancelled,
SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'Blocked') THEN 1 ELSE 0 END) AS blocked,
SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'On Hold') THEN 1 ELSE 0 END) AS on_hold,
SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'Proposed') THEN 1 ELSE 0 END) AS proposed,
SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'In Planning') THEN 1 ELSE 0 END) AS in_planning,
SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'In Progress') THEN 1 ELSE 0 END) AS in_progress,
SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'Completed') THEN 1 ELSE 0 END) AS completed
FROM projects
WHERE team_id = $1 ${archivedClause})
SELECT JSON_BUILD_OBJECT(
'all_projects', all_projects,
'cancelled', cancelled,
'blocked', blocked,
'on_hold', on_hold,
'proposed', proposed,
'in_planning', in_planning,
'in_progress', in_progress,
'completed', completed
) AS counts
FROM ProjectCounts;`;
const res = await db.query(q, [teamId]);
const [data] = res.rows;
const all = int(data?.counts.all_projects);
const cancelled = int(data?.counts.cancelled);
const blocked = int(data?.counts.blocked);
const on_hold = int(data?.counts.on_hold);
const proposed = int(data?.counts.proposed);
const in_planning = int(data?.counts.in_planning);
const in_progress = int(data?.counts.in_progress);
const completed = int(data?.counts.completed);
const chart : IChartObject[] = [];
return {
all,
cancelled,
blocked,
on_hold,
proposed,
in_planning,
in_progress,
completed,
chart
};
}
protected static async getProjectsByCategory(teamId: string | null, archivedClause = ""): Promise<any> {
const q = `
SELECT
pc.id,
pc.color_code AS color,
pc.name AS label,
COUNT(pc.id) AS count
FROM project_categories pc
JOIN projects ON pc.id = projects.category_id
WHERE projects.team_id = $1 ${archivedClause}
GROUP BY pc.id, pc.name;
`;
const result = await db.query(q, [teamId]);
const chart: IChartObject[] = [];
const total = result.rows.reduce((accumulator: number, current: {
count: number
}) => accumulator + int(current.count), 0);
for (const category of result.rows) {
category.count = int(category.count);
chart.push({
name: category.label,
color: category.color,
y: category.count
});
}
return { chart, total, data: result.rows };
}
protected static async getProjectsByHealth(teamId: string | null, archivedClause = ""): Promise<any> {
const q = `
SELECT JSON_BUILD_OBJECT(
'needs_attention', (SELECT COUNT(*)
FROM projects
WHERE team_id = $1 ${archivedClause}
AND health_id = (SELECT id FROM sys_project_healths WHERE name = 'Needs Attention')),
'at_risk', (SELECT COUNT(*)
FROM projects
WHERE team_id = $1 ${archivedClause}
AND health_id = (SELECT id FROM sys_project_healths WHERE name = 'At Risk')),
'good', (SELECT COUNT(*)
FROM projects
WHERE team_id = $1 ${archivedClause}
AND health_id = (SELECT id FROM sys_project_healths WHERE name = 'Good')),
'not_set', (SELECT COUNT(*)
FROM projects
WHERE team_id = $1 ${archivedClause}
AND health_id = (SELECT id FROM sys_project_healths WHERE name = 'Not Set'))
) AS counts;
`;
const res = await db.query(q, [teamId]);
const [data] = res.rows;
const not_set = int(data?.counts.not_set);
const needs_attention = int(data?.counts.needs_attention);
const at_risk = int(data?.counts.at_risk);
const good = int(data?.counts.good);
const chart: IChartObject[] = [];
return {
not_set,
needs_attention,
at_risk,
good,
chart
};
}
// Team Overview
protected static createByProjectStatusChartData(body: any) {
body.chart = [
this.createChartObject("Cancelled", "#f37070", body.cancelled),
this.createChartObject("Blocked", "#cbc8a1", body.blocked),
this.createChartObject("On Hold", "#cbc8a1", body.on_hold),
this.createChartObject("Proposed", "#cbc8a1", body.proposed),
this.createChartObject("In Planning", "#cbc8a1", body.in_planning),
this.createChartObject("In Progress", "#80ca79", body.in_progress),
this.createChartObject("Completed", "#80ca79", body.completed)
];
}
protected static createByProjectHealthChartData(body: any) {
body.chart = [
this.createChartObject("Not Set", "#a9a9a9", body.not_set),
this.createChartObject("Needs Attention", "#f37070", body.needs_attention),
this.createChartObject("At Risk", "#fbc84c", body.at_risk),
this.createChartObject("Good", "#75c997", body.good)
];
}
}

View File

@@ -0,0 +1,391 @@
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 db from "../../../config/db";
import { formatDuration, formatLogText, getColor, int } from "../../../shared/utils";
import ReportingOverviewBase from "./reporting-overview-base";
import { GroupBy, ITaskGroup } from "../../tasks-controller-base";
import TasksControllerV2, { TaskListGroup } from "../../tasks-controller-v2";
import { TASK_PRIORITY_COLOR_ALPHA } from "../../../shared/constants";
import { ReportingExportModel } from "../../../models/reporting-export";
import moment from "moment";
import ReportingControllerBase from "../reporting-controller-base";
export default class ReportingOverviewController extends ReportingOverviewBase {
@HandleExceptions()
public static async getStatistics(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const teamId = this.getCurrentTeamId(req);
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 user_id = '${req.user?.id}') `;
const teams = await this.getTeamsCounts(teamId, archivedClause);
const projects = await this.getProjectsCounts(teamId, archivedClause);
const members = await this.getMemberCounts(teamId);
projects.count = teams.projects;
members.count = teams.members;
const body = {
teams,
projects,
members
};
return res.status(200).send(new ServerResponse(true, body));
}
@HandleExceptions()
public static async getTeams(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const teamId = this.getCurrentTeamId(req);
const includeArchived = req.query.archived === "true";
const archivedClause = includeArchived
? ""
: `AND 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,
COALESCE((SELECT COUNT(*) FROM projects WHERE team_id = teams.id ${archivedClause}), 0) AS projects_count,
(SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
FROM (
--
SELECT (SELECT name
FROM team_member_info_view
WHERE team_member_info_view.team_member_id = tm.id),
u.avatar_url
FROM team_members tm
LEFT JOIN users u ON tm.user_id = u.id
WHERE team_id = teams.id
--
) rec) AS members
FROM teams
WHERE in_organization(id, $1)
ORDER BY name;
`;
const result = await db.query(q, [teamId]);
for (const team of result.rows) {
team.members = this.createTagList(team?.members);
team.members.map((a: any) => a.color_code = getColor(a.name));
}
return res.status(200).send(new ServerResponse(true, result.rows));
}
@HandleExceptions()
public static async getProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { searchQuery, sortField, sortOrder, size, offset } = this.toPaginationOptions(req.query, ["p.name"]);
const archived = req.query.archived === "true";
const teamId = req.query.team as string;
const archivedClause = archived
? ""
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
const teamFilterClause = `p.team_id = $1`;
const result = await ReportingControllerBase.getProjectsByTeam(teamId, size, offset, searchQuery, sortField, sortOrder, "", "", "", archivedClause, teamFilterClause, "");
for (const project of result.projects) {
project.team_color = getColor(project.team_name) + TASK_PRIORITY_COLOR_ALPHA;
project.days_left = ReportingControllerBase.getDaysLeft(project.end_date);
project.is_overdue = ReportingControllerBase.isOverdue(project.end_date);
if (project.days_left && project.is_overdue) {
project.days_left = project.days_left.toString().replace(/-/g, "");
}
project.is_today = this.isToday(project.end_date);
project.estimated_time = int(project.estimated_time);
project.actual_time = int(project.actual_time);
project.estimated_time_string = this.convertMinutesToHoursAndMinutes(int(project.estimated_time));
project.actual_time_string = this.convertSecondsToHoursAndMinutes(int(project.actual_time));
project.tasks_stat = {
todo: this.getPercentage(int(project.tasks_stat.todo), +project.tasks_stat.total),
doing: this.getPercentage(int(project.tasks_stat.doing), +project.tasks_stat.total),
done: this.getPercentage(int(project.tasks_stat.done), +project.tasks_stat.total)
};
if (project.update.length > 0) {
const update = project.update[0];
const placeHolders = update.content.match(/{\d+}/g);
if (placeHolders) {
placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
const index = parseInt(placeHolder.match(/\d+/)[0]);
if (index >= 0 && index < update.mentions.length) {
update.content = update.content.replace(placeHolder, `
<span class='mentions'> @${update.mentions[index].user_name} </span>`);
}
});
}
project.comment = update.content;
}
if (project.last_activity) {
if (project.last_activity.attribute_type === "estimation") {
project.last_activity.previous = formatDuration(moment.duration(project.last_activity.previous, "minutes"));
project.last_activity.current = formatDuration(moment.duration(project.last_activity.current, "minutes"));
}
if (project.last_activity.assigned_user) project.last_activity.assigned_user.color_code = getColor(project.last_activity.assigned_user.name);
project.last_activity.done_by.color_code = getColor(project.last_activity.done_by.name);
project.last_activity.log_text = await formatLogText(project.last_activity);
project.last_activity.attribute_type = project.last_activity.attribute_type?.replace(/_/g, " ");
project.last_activity.last_activity_string = `${project.last_activity.done_by.name} ${project.last_activity.log_text} ${project.last_activity.attribute_type}`;
}
}
return res.status(200).send(new ServerResponse(true, result));
}
@HandleExceptions()
public static async getProjectsByTeamOrMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const teamId = req.params.team_id?.trim() || null;
const teamMemberId = (req.query.member as string)?.trim() || null;
const teamMemberFilter = teamId === "undefined" ? `AND pm.team_member_id = $1` : teamMemberId ? `AND pm.team_member_id = $2` : "";
const teamIdFilter = teamId === "undefined" ? "p.team_id IS NOT NULL" : `p.team_id = $1`;
const q = `
SELECT p.id,
p.name,
p.color_code,
p.team_id,
p.status_id
FROM projects p
LEFT JOIN project_members pm ON pm.project_id = p.id
WHERE ${teamIdFilter} ${teamMemberFilter}
GROUP BY p.id, p.name;`;
const params = teamId === "undefined" ? [teamMemberId] : teamMemberId ? [teamId, teamMemberId] : [teamId];
const result = await db.query(q, params);
const data = result.rows;
return res.status(200).send(new ServerResponse(true, data));
}
@HandleExceptions()
public static async getMembersByTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const teamId = req.params.team_id?.trim() || null;
const archived = req.query.archived === "true";
const pmArchivedClause = archived ? `` : `AND project_members.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = project_members.project_id AND user_id = '${req.user?.id}')`;
const taArchivedClause = archived ? `` : `AND (SELECT tasks.project_id FROM tasks WHERE tasks.id = tasks_assignees.task_id) NOT IN (SELECT project_id FROM archived_projects WHERE project_id = (SELECT tasks.project_id FROM tasks WHERE tasks.id = tasks_assignees.task_id) AND user_id = '${req.user?.id}')`;
const q = `
SELECT team_member_id AS id,
name,
email,
(SELECT COUNT(*)
FROM project_members
WHERE project_members.team_member_id = team_member_info_view.team_member_id ${pmArchivedClause}) AS projects,
(SELECT COUNT(*)
FROM tasks_assignees
WHERE tasks_assignees.team_member_id = team_member_info_view.team_member_id ${taArchivedClause}) AS tasks,
(SELECT COUNT(*)
FROM tasks_assignees
WHERE tasks_assignees.team_member_id = team_member_info_view.team_member_id
AND is_overdue(task_id) IS TRUE ${taArchivedClause}) AS overdue,
(SELECT COUNT(*)
FROM tasks_assignees
WHERE tasks_assignees.team_member_id = team_member_info_view.team_member_id
AND task_id IN (SELECT id
FROM tasks
WHERE is_completed(tasks.status_id, tasks.project_id)) ${taArchivedClause}) AS completed,
(SELECT COUNT(*)
FROM tasks_assignees
WHERE tasks_assignees.team_member_id = team_member_info_view.team_member_id
AND task_id IN (SELECT id
FROM tasks
WHERE is_doing(tasks.status_id, tasks.project_id)) ${taArchivedClause}) AS ongoing
FROM team_member_info_view
WHERE team_id = $1
ORDER BY name;
`;
const result = await db.query(q, [teamId]);
for (const member of result.rows) {
member.projects = int(member.projects);
member.tasks = int(member.tasks);
member.overdue = int(member.overdue);
member.completed = int(member.completed);
member.ongoing = int(member.ongoing);
}
return res.status(200).send(new ServerResponse(true, result.rows));
}
@HandleExceptions()
public static async getProjectOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const projectId = req.params.project_id || null;
const stats = await this.getProjectStats(projectId);
const byStatus = await this.getTasksByStatus(projectId);
const byPriority = await this.getTasksByPriority(projectId);
const byDue = await this.getTaskCountsByDue(projectId);
byPriority.all = byStatus.all;
byDue.all = byStatus.all;
byDue.completed = stats.completed;
byDue.overdue = stats.overdue;
const body = {
stats,
by_status: byStatus,
by_priority: byPriority,
by_due: byDue
};
this.createByStatusChartData(body.by_status);
this.createByPriorityChartData(body.by_priority);
this.createByDueDateChartData(body.by_due);
return res.status(200).send(new ServerResponse(true, body));
}
@HandleExceptions()
public static async getProjectMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const projectId = req.params.project_id?.trim() || null;
const members = await ReportingExportModel.getProjectMembers(projectId as string);
return res.status(200).send(new ServerResponse(true, members));
}
@HandleExceptions()
public static async getProjectTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const groupBy = (req.query.group || GroupBy.STATUS) as string;
const projectId = req.params.project_id?.trim() || null;
const groups = await TasksControllerV2.getGroups(groupBy, projectId as string);
const tasks = await this.getAllTasks(projectId);
const map = groups.reduce((g: { [x: string]: ITaskGroup }, group) => {
if (group.id)
g[group.id] = new TaskListGroup(group);
return g;
}, {});
TasksControllerV2.updateMapByGroup(tasks, groupBy, map);
const updatedGroups = Object.keys(map).map(key => {
const group = map[key];
if (groupBy === GroupBy.PHASE)
group.color_code = getColor(group.name) + TASK_PRIORITY_COLOR_ALPHA;
return {
id: key,
...group
};
});
return res.status(200).send(new ServerResponse(true, updatedGroups));
}
@HandleExceptions()
public static async getTeamMemberOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const teamMemberId = req.query.teamMemberId as string;
const archived = req.query.archived === "true";
const stats = await this.getTeamMemberStats(teamMemberId, archived, req.user?.id as string);
const byStatus = await this.getTasksByStatusOfTeamMemberOverview(teamMemberId, archived, req.user?.id as string);
const byProject = await this.getTasksByProjectOfTeamMemberOverview(teamMemberId, archived, req.user?.id as string);
const byPriority = await this.getTasksByPriorityOfTeamMemberOverview(teamMemberId, archived, req.user?.id as string);
stats.projects = await this.getProjectCountOfTeamMember(teamMemberId, archived, req.user?.id as string);
const body = {
stats,
by_status: byStatus,
by_project: byProject,
by_priority: byPriority
};
return res.status(200).send(new ServerResponse(true, body));
}
@HandleExceptions()
public static async getMemberOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const teamMemberId = req.query.teamMemberId as string;
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 stats = await this.getMemberStats(teamMemberId, duration as string, dateRange, archived, req.user?.id as string);
const byStatus = await this.getTasksByStatusOfTeamMember(teamMemberId, duration as string, dateRange, archived, req.user?.id as string);
const byProject = await this.getTasksByProjectOfTeamMember(teamMemberId, duration as string, dateRange, archived, req.user?.id as string);
const byPriority = await this.getTasksByPriorityOfTeamMember(teamMemberId, duration as string, dateRange, archived, req.user?.id as string);
stats.teams = await this.getTeamCountOfTeamMember(teamMemberId);
stats.projects = await this.getProjectCountOfTeamMember(teamMemberId, archived, req.user?.id as string);
const body = {
stats,
by_status: byStatus,
by_project: byProject,
by_priority: byPriority
};
return res.status(200).send(new ServerResponse(true, body));
}
@HandleExceptions()
public static async getMemberTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const teamMemberId = req.params.team_member_id?.trim() || null;
const projectId = (req.query.project as string)?.trim() || null;
const onlySingleMember = req.query.only_single_member as string;
const { duration, date_range } = req.query;
const includeArchived = req.query.archived === "true";
let dateRange: string[] = [];
if (typeof date_range === "string") {
dateRange = date_range.split(",");
}
const results = await ReportingExportModel.getMemberTasks(teamMemberId as string, projectId, onlySingleMember, duration as string, dateRange, includeArchived, req.user?.id as string);
return res.status(200).send(new ServerResponse(true, results));
}
@HandleExceptions()
public static async getTeamOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const teamId = req.params.team_id || null;
const archived = req.query.archived === "true";
const archivedClause = await this.getArchivedProjectsClause(archived, req.user?.id as string, "projects.id");
const byStatus = await this.getProjectsByStatus(teamId, archivedClause);
const byCategory = await this.getProjectsByCategory(teamId, archivedClause);
const byHealth = await this.getProjectsByHealth(teamId, archivedClause);
byCategory.all = byStatus.all;
byHealth.all = byStatus.all;
const body = {
by_status: byStatus,
by_category: byCategory,
by_health: byHealth
};
this.createByProjectStatusChartData(body.by_status);
this.createByProjectHealthChartData(body.by_health);
return res.status(200).send(new ServerResponse(true, body));
}
}

View File

@@ -0,0 +1,581 @@
import HandleExceptions from "../../../decorators/handle-exceptions";
import { IWorkLenzRequest } from "../../../interfaces/worklenz-request";
import { IWorkLenzResponse } from "../../../interfaces/worklenz-response";
import ReportingOverviewBase from "./reporting-overview-base";
import { ReportingExportModel } from "../../../models/reporting-export";
import { formatDuration, formatLogText, getColor, int } from "../../../shared/utils";
import moment from "moment";
import Excel from "exceljs";
import ReportingControllerBase from "../reporting-controller-base";
import { TASK_PRIORITY_COLOR_ALPHA } from "../../../shared/constants";
export default class ReportingOverviewExportController extends ReportingOverviewBase {
@HandleExceptions()
public static async getProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { searchQuery, sortField, sortOrder, size, offset } = this.toPaginationOptions(req.query, ["p.name"]);
const archived = req.query.archived === "true";
const teamId = req.query.team as string;
const archivedClause = archived
? ""
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
const teamFilterClause = `p.team_id = $1`;
const result = await ReportingControllerBase.getProjectsByTeam(teamId, size, offset, searchQuery, sortField, sortOrder, "", "", "", archivedClause, teamFilterClause, "");
for (const project of result.projects) {
project.team_color = getColor(project.team_name) + TASK_PRIORITY_COLOR_ALPHA;
project.days_left = ReportingControllerBase.getDaysLeft(project.end_date);
project.is_overdue = ReportingControllerBase.isOverdue(project.end_date);
if (project.days_left && project.is_overdue) {
project.days_left = project.days_left.toString().replace(/-/g, "");
}
project.is_today = this.isToday(project.end_date);
project.estimated_time = int(project.estimated_time);
project.actual_time = int(project.actual_time);
project.estimated_time_string = this.convertMinutesToHoursAndMinutes(int(project.estimated_time));
project.actual_time_string = this.convertSecondsToHoursAndMinutes(int(project.actual_time));
project.tasks_stat = {
todo: this.getPercentage(int(project.tasks_stat.todo), +project.tasks_stat.total),
doing: this.getPercentage(int(project.tasks_stat.doing), +project.tasks_stat.total),
done: this.getPercentage(int(project.tasks_stat.done), +project.tasks_stat.total)
};
if (project.update.length > 0) {
const update = project.update[0];
const placeHolders = update.content.match(/{\d+}/g);
if (placeHolders) {
placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
const index = parseInt(placeHolder.match(/\d+/)[0]);
if (index >= 0 && index < update.mentions.length) {
update.content = update.content.replace(placeHolder, `
<span class='mentions'> @${update.mentions[index].user_name} </span>`);
}
});
}
project.comment = update.content;
}
if (project.last_activity) {
if (project.last_activity.attribute_type === "estimation") {
project.last_activity.previous = formatDuration(moment.duration(project.last_activity.previous, "minutes"));
project.last_activity.current = formatDuration(moment.duration(project.last_activity.current, "minutes"));
}
if (project.last_activity.assigned_user) project.last_activity.assigned_user.color_code = getColor(project.last_activity.assigned_user.name);
project.last_activity.done_by.color_code = getColor(project.last_activity.done_by.name);
project.last_activity.log_text = await formatLogText(project.last_activity);
project.last_activity.attribute_type = project.last_activity.attribute_type?.replace(/_/g, " ");
project.last_activity.last_activity_string = `${project.last_activity.done_by.name} ${project.last_activity.log_text} ${project.last_activity.attribute_type}`;
}
}
return result;
}
@HandleExceptions()
public static async getProjectsByTeamOrMember(req: IWorkLenzRequest, res: IWorkLenzResponse) {
const teamId = (req.query.team_id as string)?.trim() || null;
const teamName = (req.query.team_name as string)?.trim() || null;
const result = await ReportingControllerBase.exportProjects(teamId as string);
// excel file
const exportDate = moment().format("MMM-DD-YYYY");
const fileName = `${teamName} projects - ${exportDate}`;
const workbook = new Excel.Workbook();
const sheet = workbook.addWorksheet("Projects");
// define columns in table
sheet.columns = [
{ header: "Project", key: "name", width: 30 },
{ header: "Client", key: "client", width: 20 },
{ header: "Category", key: "category", width: 20 },
{ header: "Status", key: "status", width: 20 },
{ header: "Start Date", key: "start_date", width: 20 },
{ header: "End Date", key: "end_date", width: 20 },
{ header: "Days Left/Overdue", key: "days_left", width: 20 },
{ header: "Estimated Hours", key: "estimated_hours", width: 20 },
{ header: "Actual Hours", key: "actual_hours", 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 },
{ header: "Last Activity", key: "last_activity", width: 20 },
{ header: "Project Health", key: "project_health", width: 20 },
{ header: "Project Update", key: "project_update", width: 20 }
];
// set title
sheet.getCell("A1").value = `Projects from ${teamName}`;
sheet.mergeCells("A1:O1");
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:O2");
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", "Client", "Category", "Status", "Start Date", "End Date", "Days Left/Overdue", "Estimated Hours", "Actual Hours", "Done Tasks(%)", "Doing Tasks(%)", "Todo Tasks(%)", "Last Activity", "Project Health", "Project Update"];
sheet.getRow(4).font = { bold: true };
// set table data
for (const item of result.projects) {
if (item.is_overdue && item.days_left) {
item.days_left = `-${item.days_left}`;
}
if (item.is_today) {
item.days_left = `Today`;
}
sheet.addRow({
name: item.name,
client: item.client ? item.client : "-",
category: item.category_name ? item.category_name : "-",
status: item.status_name ? item.status_name : "-",
start_date: item.start_date ? moment(item.start_date).format("YYYY-MM-DD") : "-",
end_date: item.end_date ? moment(item.end_date).format("YYYY-MM-DD") : "-",
days_left: item.days_left ? item.days_left.toString() : "-",
estimated_hours: item.estimated_time ? item.estimated_time.toString() : "-",
actual_hours: item.actual_time ? item.actual_time.toString() : "-",
done_tasks: item.tasks_stat.done ? `${item.tasks_stat.done}` : "-",
doing_tasks: item.tasks_stat.doing ? `${item.tasks_stat.doing}` : "-",
todo_tasks: item.tasks_stat.todo ? `${item.tasks_stat.todo}` : "-",
last_activity: item.last_activity ? item.last_activity.last_activity_string : "-",
project_health: item.project_health,
project_update: item.comment ? item.comment : "-",
});
}
// 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 getMembersByTeam(req: IWorkLenzRequest, res: IWorkLenzResponse) {
const teamId = (req.query.team_id as string)?.trim() || null;
const teamName = (req.query.team_name as string)?.trim() || null;
const result = await ReportingExportModel.getMembersByTeam(teamId);
for (const member of result) {
member.projects = int(member.projects);
member.tasks = int(member.tasks);
member.overdue = int(member.overdue);
member.completed = int(member.completed);
member.ongoing = int(member.ongoing);
}
// excel file
const exportDate = moment().format("MMM-DD-YYYY");
const fileName = `${teamName} members - ${exportDate}`;
const workbook = new Excel.Workbook();
const sheet = workbook.addWorksheet("Members");
// define columns in table
sheet.columns = [
{ header: "Name", key: "name", width: 30 },
{ header: "Email", key: "email", width: 20 },
{ header: "Projects", key: "projects", width: 20 },
{ header: "Tasks", 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 },
];
// set title
sheet.getCell("A1").value = `Members from ${teamName}`;
sheet.mergeCells("A1:G1");
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:G2");
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 = ["Name", "Email", "Projects", "Tasks", "Overdue Tasks", "Completed Tasks", "Ongoing Tasks"];
sheet.getRow(4).font = { bold: true };
// set table data
for (const item of result) {
sheet.addRow({
name: item.name,
email: item.email ? item.email : "-",
projects: item.projects ? item.projects.toString() : "-",
tasks: item.tasks ? item.tasks.toString() : "-",
overdue_tasks: item.overdue ? item.overdue.toString() : "-",
completed_tasks: item.completed ? item.completed.toString() : "-",
ongoing_tasks: item.ongoing ? item.ongoing.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();
});
}
@HandleExceptions()
public static async exportProjectMembers(req: IWorkLenzRequest, res: IWorkLenzResponse) {
const projectId = (req.query.project_id as string)?.trim() || null;
const projectName = (req.query.project_name as string)?.trim() || null;
const teamName = (req.query.team_name as string)?.trim() || "";
const results = await ReportingExportModel.getProjectMembers(projectId as string);
// excel file
const exportDate = moment().format("MMM-DD-YYYY");
const fileName = `${teamName} ${projectName} members - ${exportDate}`;
const workbook = new Excel.Workbook();
const sheet = workbook.addWorksheet("Members");
// define columns in table
sheet.columns = [
{ header: "Name", key: "name", width: 30 },
{ header: "Tasks Count", key: "tasks_count", width: 20 },
{ header: "Completed Tasks", key: "completed_tasks", width: 20 },
{ header: "Incomplete Tasks", key: "incomplete_tasks", width: 20 },
{ header: "Overdue Tasks", key: "overdue_tasks", width: 20 },
{ header: "Contribution(%)", key: "contribution", width: 20 },
{ header: "Progress(%)", key: "progress", width: 20 },
{ header: "Logged Time", key: "logged_time", width: 20 },
];
// set title
sheet.getCell("A1").value = `Members from ${projectName} - ${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 = ["Name", "Tasks Count", "Completed Tasks", "Incomplete Tasks", "Overdue Tasks", "Contribution(%)", "Progress(%)", "Logged Time"];
sheet.getRow(4).font = { bold: true };
// set table data
for (const item of results) {
sheet.addRow({
name: item.name,
tasks_count: item.tasks_count ? item.tasks_count : "-",
completed_tasks: item.completed ? item.completed : "-",
incomplete_tasks: item.incompleted ? item.incompleted : "-",
overdue_tasks: item.overdue ? item.overdue : "-",
contribution: item.contribution ? item.contribution : "-",
progress: item.progress ? item.progress : "-",
logged_time: item.time_logged ? item.time_logged : "-",
});
}
// 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 exportProjectTasks(req: IWorkLenzRequest, res: IWorkLenzResponse) {
const projectId = (req.query.project_id as string)?.trim() || null;
const projectName = (req.query.project_name as string)?.trim() || null;
const teamName = (req.query.team_name as string)?.trim() || "";
const results = await this.getAllTasks(projectId);
// excel file
const exportDate = moment().format("MMM-DD-YYYY");
const fileName = `${teamName} ${projectName} tasks - ${exportDate}`;
const workbook = new Excel.Workbook();
const sheet = workbook.addWorksheet("Tasks");
// define columns in table
sheet.columns = [
{ header: "Task", key: "task", width: 30 },
{ header: "Status", key: "status", width: 20 },
{ header: "Priority", key: "priority", width: 20 },
{ header: "Phase", key: "phase", width: 20 },
{ header: "Due Date", key: "due_date", width: 20 },
{ header: "Completed On", key: "completed_on", width: 20 },
{ header: "Days Overdue", key: "days_overdue", width: 20 },
{ header: "Estimated Time", key: "estimated_time", width: 20 },
{ header: "Logged Time", key: "logged_time", width: 20 },
{ header: "Overlogged Time", key: "overlogged_time", width: 20 },
];
// set title
sheet.getCell("A1").value = `Tasks from ${projectName} - ${teamName}`;
sheet.mergeCells("A1:J1");
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:J2");
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 = ["Task", "Status", "Priority", "Phase", "Due Date", "Completed On", "Days Overdue", "Estimated Time", "Logged Time", "Overlogged Time"];
sheet.getRow(4).font = { bold: true };
// set table data
for (const item of results) {
const time_spent = { hours: ~~(item.total_minutes_spent / 60), minutes: item.total_minutes_spent % 60 };
item.total_minutes_spent = Math.ceil(item.total_seconds_spent / 60);
sheet.addRow({
task: item.name,
status: item.status_name ? item.status_name : "-",
priority: item.priority_name ? item.priority_name : "-",
phase: item.phase_name ? item.phase_name : "-",
due_date: item.end_date ? moment(item.end_date).format("YYYY-MM-DD") : "-",
completed_on: item.completed_at ? moment(item.completed_at).format("YYYY-MM-DD") : "-",
days_overdue: item.overdue_days ? item.overdue_days : "-",
estimated_time: item.total_minutes !== "0" ? `${~~(item.total_minutes / 60)}h ${(item.total_minutes % 60)}m` : "-",
logged_time: item.total_minutes_spent ? `${time_spent.hours}h ${(time_spent.minutes)}m` : "-",
overlogged_time: item.overlogged_time_string !== "0h 0m" ? item.overlogged_time_string : "-",
});
}
// 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 exportMemberTasks(req: IWorkLenzRequest, res: IWorkLenzResponse) {
const teamMemberId = (req.query.team_member_id as string)?.trim() || null;
const teamMemberName = (req.query.team_member_name as string)?.trim() || null;
const teamName = (req.query.team_name as string)?.trim() || "";
const { duration, date_range, only_single_member, archived} = req.query;
const includeArchived = req.query.archived === "true";
let dateRange: string[] = [];
if (typeof date_range === "string") {
dateRange = date_range.split(",");
}
const results = await ReportingExportModel.getMemberTasks(teamMemberId as string, null, only_single_member as string, duration as string, dateRange, includeArchived, req.user?.id as string);
// excel file
const exportDate = moment().format("MMM-DD-YYYY");
const fileName = `${teamMemberName} tasks - ${exportDate}`;
const workbook = new Excel.Workbook();
const sheet = workbook.addWorksheet("Tasks");
// define columns in table
sheet.columns = [
{ header: "Task", key: "task", width: 30 },
{ header: "Project", key: "project", width: 20 },
{ header: "Status", key: "status", width: 20 },
{ header: "Priority", key: "priority", width: 20 },
{ header: "Due Date", key: "due_date", width: 20 },
{ header: "Completed Date", key: "completed_on", width: 20 },
{ header: "Estimated Time", key: "estimated_time", width: 20 },
{ header: "Logged Time", key: "logged_time", width: 20 },
{ header: "Overlogged Time", key: "overlogged_time", width: 20 },
];
// set title
sheet.getCell("A1").value = `Tasks of ${teamMemberName} - ${teamName}`;
sheet.mergeCells("A1:I1");
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:I2");
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 = ["Task", "Project", "Status", "Priority", "Due Date", "Completed Date", "Estimated Time", "Logged Time", "Overlogged Time"];
sheet.getRow(4).font = { bold: true };
// set table data
for (const item of results) {
sheet.addRow({
task: item.name,
project: item.project_name ? item.project_name : "-",
status: item.status_name ? item.status_name : "-",
priority: item.priority_name ? item.priority_name : "-",
due_date: item.end_date ? moment(item.end_date).format("YYYY-MM-DD") : "-",
completed_on: item.completed_date ? moment(item.completed_date).format("YYYY-MM-DD") : "-",
estimated_time: item.estimated_string ? item.estimated_string : "-",
logged_time: item.time_spent_string ? item.time_spent_string : "-",
overlogged_time: item.overlogged_time ? item.overlogged_time : "-",
});
}
// 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 exportFlatTasks(req: IWorkLenzRequest, res: IWorkLenzResponse) {
const teamMemberId = (req.query.team_member_id as string)?.trim() || null;
const teamMemberName = (req.query.team_member_name as string)?.trim() || null;
const projectId = (req.query.project_id as string)?.trim() || null;
const projectName = (req.query.project_name as string)?.trim() || null;
const includeArchived = req.query.archived === "true";
const results = await ReportingExportModel.getMemberTasks(teamMemberId as string, projectId, "false", "", [], includeArchived, req.user?.id as string);
// excel file
const exportDate = moment().format("MMM-DD-YYYY");
const fileName = `${teamMemberName}'s tasks in ${projectName} - ${exportDate}`;
const workbook = new Excel.Workbook();
const sheet = workbook.addWorksheet("Tasks");
// define columns in table
sheet.columns = [
{ header: "Task", key: "task", width: 30 },
{ header: "Project", key: "project", width: 20 },
{ header: "Status", key: "status", width: 20 },
{ header: "Priority", key: "priority", width: 20 },
{ header: "Due Date", key: "due_date", width: 20 },
{ header: "Completed Date", key: "completed_on", width: 20 },
{ header: "Estimated Time", key: "estimated_time", width: 20 },
{ header: "Logged Time", key: "logged_time", width: 20 },
{ header: "Overlogged Time", key: "overlogged_time", width: 20 },
];
// set title
sheet.getCell("A1").value = `Tasks of ${teamMemberName} in ${projectName}`;
sheet.mergeCells("A1:I1");
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:I2");
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 = ["Task", "Project", "Status", "Priority", "Due Date", "Completed Date", "Estimated Time", "Logged Time", "Overlogged Time"];
sheet.getRow(4).font = { bold: true };
// set table data
for (const item of results) {
sheet.addRow({
task: item.name,
project: item.project_name ? item.project_name : "-",
status: item.status_name ? item.status_name : "-",
priority: item.priority_name ? item.priority_name : "-",
due_date: item.end_date ? moment(item.end_date).format("YYYY-MM-DD") : "-",
completed_on: item.completed_date ? moment(item.completed_date).format("YYYY-MM-DD") : "-",
estimated_time: item.estimated_string ? item.estimated_string : "-",
logged_time: item.time_spent_string ? item.time_spent_string : "-",
overlogged_time: item.overlogged_time ? item.overlogged_time : "-",
});
}
// 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();
});
}
}

View File

@@ -0,0 +1,4 @@
import ReportingControllerBase from "../reporting-controller-base";
export default class ReportingProjectsBase extends ReportingControllerBase {
}

View File

@@ -0,0 +1,218 @@
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 ReportingProjectsBase from "./reporting-projects-base";
import ReportingControllerBase from "../reporting-controller-base";
import moment from "moment";
import { DATE_RANGES, TASK_PRIORITY_COLOR_ALPHA } from "../../../shared/constants";
import { getColor, int, formatDuration, formatLogText } from "../../../shared/utils";
import db from "../../../config/db";
export default class ReportingProjectsController extends ReportingProjectsBase {
private static flatString(text: string) {
return (text || "").split(",").map(s => `'${s}'`).join(",");
}
@HandleExceptions()
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { searchQuery, sortField, sortOrder, size, offset } = this.toPaginationOptions(req.query, ["p.name"]);
const archived = req.query.archived === "true";
const teamId = this.getCurrentTeamId(req);
const statusesClause = req.query.statuses as string
? `AND p.status_id IN (${this.flatString(req.query.statuses as string)})`
: "";
const healthsClause = req.query.healths as string
? `AND p.health_id IN (${this.flatString(req.query.healths as string)})`
: "";
const categoriesClause = req.query.categories as string
? `AND p.category_id IN (${this.flatString(req.query.categories as string)})`
: "";
// const projectManagersClause = req.query.project_managers as string
// ? `AND p.id IN (SELECT project_id from project_members WHERE team_member_id IN (${this.flatString(req.query.project_managers as string)}) AND project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER'))`
// : "";
const projectManagersClause = req.query.project_managers as string
? `AND p.id IN(SELECT project_id FROM project_members WHERE team_member_id IN(SELECT id FROM team_members WHERE user_id IN (${this.flatString(req.query.project_managers as string)})) AND project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER'))`
: "";
const archivedClause = archived
? ""
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
const teamFilterClause = `in_organization(p.team_id, $1)`;
const result = await ReportingControllerBase.getProjectsByTeam(teamId as string, size, offset, searchQuery, sortField, sortOrder, statusesClause, healthsClause, categoriesClause, archivedClause, teamFilterClause, projectManagersClause);
for (const project of result.projects) {
project.team_color = getColor(project.team_name) + TASK_PRIORITY_COLOR_ALPHA;
project.days_left = ReportingControllerBase.getDaysLeft(project.end_date);
project.is_overdue = ReportingControllerBase.isOverdue(project.end_date);
if (project.days_left && project.is_overdue) {
project.days_left = project.days_left.toString().replace(/-/g, "");
}
project.is_today = this.isToday(project.end_date);
project.estimated_time = int(project.estimated_time);
project.actual_time = int(project.actual_time);
project.estimated_time_string = this.convertMinutesToHoursAndMinutes(int(project.estimated_time));
project.actual_time_string = this.convertSecondsToHoursAndMinutes(int(project.actual_time));
project.tasks_stat = {
todo: this.getPercentage(int(project.tasks_stat.todo), +project.tasks_stat.total),
doing: this.getPercentage(int(project.tasks_stat.doing), +project.tasks_stat.total),
done: this.getPercentage(int(project.tasks_stat.done), +project.tasks_stat.total)
};
if (project.update.length > 0) {
const [update] = project.update;
const placeHolders = update.content.match(/{\d+}/g);
if (placeHolders) {
placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
const index = parseInt(placeHolder.match(/\d+/)[0]);
if (index >= 0 && index < update.mentions.length) {
update.content = update.content.replace(placeHolder, `
<span class='mentions'> @${update.mentions[index].user_name} </span>`);
}
});
}
project.comment = update.content;
}
if (project.last_activity) {
if (project.last_activity.attribute_type === "estimation") {
project.last_activity.previous = formatDuration(moment.duration(project.last_activity.previous, "minutes"));
project.last_activity.current = formatDuration(moment.duration(project.last_activity.current, "minutes"));
}
if (project.last_activity.assigned_user) project.last_activity.assigned_user.color_code = getColor(project.last_activity.assigned_user.name);
project.last_activity.done_by.color_code = getColor(project.last_activity.done_by.name);
project.last_activity.log_text = await formatLogText(project.last_activity);
project.last_activity.attribute_type = project.last_activity.attribute_type?.replace(/_/g, " ");
project.last_activity.last_activity_string = `${project.last_activity.done_by.name} ${project.last_activity.log_text} ${project.last_activity.attribute_type}`;
}
}
return res.status(200).send(new ServerResponse(true, result));
}
protected static getMinMaxDates(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");
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(task_work_log.created_at)::DATE) FROM task_work_log WHERE task_id IN (SELECT id FROM tasks WHERE project_id = $1)) AS start_date, (SELECT (MAX(task_work_log.created_at)::DATE) FROM task_work_log WHERE task_id IN (SELECT id FROM tasks WHERE project_id = $1)) AS end_date";
return "";
}
@HandleExceptions()
public static async getProjectTimeLogs(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const projectId = req.body.id;
const { duration, date_range } = req.body;
const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range);
const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range);
const q = `SELECT
(SELECT name FROM projects WHERE projects.id = $1) AS project_name,
(SELECT key FROM projects WHERE projects.id = $1) AS project_key,
(SELECT task_no FROM tasks WHERE tasks.id = task_work_log.task_id) AS task_key_num,
(SELECT name FROM tasks WHERE tasks.id = task_work_log.task_id) AS task_name,
task_work_log.time_spent,
(SELECT name FROM users WHERE users.id = task_work_log.user_id) AS user_name,
(SELECT email FROM users WHERE users.id = task_work_log.user_id) AS user_email,
(SELECT avatar_url FROM users WHERE users.id = task_work_log.user_id) AS avatar_url,
task_work_log.created_at
${minMaxDateClause}
FROM task_work_log
WHERE
task_id IN (select id from tasks WHERE project_id = $1)
${durationClause}
ORDER BY task_work_log.created_at DESC`;
const result = await db.query(q, [projectId]);
const formattedResult = await this.formatLog(result.rows);
const logGroups = await this.getTimeLogDays(formattedResult);
return res.status(200).send(new ServerResponse(true, logGroups));
}
private static async formatLog(result: any[]) {
result.forEach((row) => {
const duration = moment.duration(row.time_spent, "seconds");
row.time_spent_string = this.formatDuration(duration);
row.task_key = `${row.project_key}-${row.task_key_num}`;
});
return result;
}
private static async getTimeLogDays(result: any[]) {
if (result.length) {
const startDate = moment(result[0].start_date).isValid() ? moment(result[0].start_date, "YYYY-MM-DD").clone() : null;
const endDate = moment(result[0].end_date).isValid() ? moment(result[0].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.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 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;
}
}

View File

@@ -0,0 +1,279 @@
import moment from "moment";
import HandleExceptions from "../../../decorators/handle-exceptions";
import { IWorkLenzRequest } from "../../../interfaces/worklenz-request";
import { IWorkLenzResponse } from "../../../interfaces/worklenz-response";
import ReportingProjectsBase from "./reporting-projects-base";
import Excel from "exceljs";
import ReportingControllerBase from "../reporting-controller-base";
import { DATE_RANGES } from "../../../shared/constants";
import db from "../../../config/db";
export default class ReportingProjectsExportController extends ReportingProjectsBase {
@HandleExceptions()
public static async export(req: IWorkLenzRequest, res: IWorkLenzResponse) {
const teamId = this.getCurrentTeamId(req);
const teamName = (req.query.team_name as string)?.trim() || null;
const results = await ReportingControllerBase.exportProjectsAll(teamId as string);
// excel file
const exportDate = moment().format("MMM-DD-YYYY");
const fileName = `${teamName} projects - ${exportDate}`;
const workbook = new Excel.Workbook();
const sheet = workbook.addWorksheet("Projects");
// define columns in table
sheet.columns = [
{ header: "Project", key: "name", width: 30 },
{ header: "Client", key: "client", width: 20 },
{ header: "Category", key: "category", width: 20 },
{ header: "Status", key: "status", width: 20 },
{ header: "Start Date", key: "start_date", width: 20 },
{ header: "End Date", key: "end_date", width: 20 },
{ header: "Days Left/Overdue", key: "days_left", width: 20 },
{ header: "Estimated Hours", key: "estimated_hours", width: 20 },
{ header: "Actual Hours", key: "actual_hours", 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 },
{ header: "Last Activity", key: "last_activity", width: 20 },
{ header: "Project Health", key: "project_health", width: 20 },
{ header: "Project Update", key: "project_update", width: 20 }
];
// set title
sheet.getCell("A1").value = `Projects from ${teamName}`;
sheet.mergeCells("A1:O1");
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:O2");
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", "Client", "Category", "Status", "Start Date", "End Date", "Days Left/Overdue", "Estimated Hours", "Actual Hours", "Done Tasks(%)", "Doing Tasks(%)", "Todo Tasks(%)", "Last Activity", "Project Health", "Project Update"];
sheet.getRow(4).font = { bold: true };
// set table data
for (const item of results.projects) {
if (item.is_overdue && item.days_left) {
item.days_left = `-${item.days_left}`;
}
if (item.is_today) {
item.days_left = `Today`;
}
sheet.addRow({
name: item.name,
client: item.client ? item.client : "-",
category: item.category_name ? item.category_name : "-",
status: item.status_name ? item.status_name : "-",
start_date: item.start_date ? moment(item.start_date).format("YYYY-MM-DD") : "-",
end_date: item.end_date ? moment(item.end_date).format("YYYY-MM-DD") : "-",
days_left: item.days_left ? item.days_left.toString() : "-",
estimated_hours: item.estimated_time ? item.estimated_time.toString() : "-",
actual_hours: item.actual_time ? item.actual_time.toString() : "-",
done_tasks: item.tasks_stat.done ? `${item.tasks_stat.done}` : "-",
doing_tasks: item.tasks_stat.doing ? `${item.tasks_stat.doing}` : "-",
todo_tasks: item.tasks_stat.todo ? `${item.tasks_stat.todo}` : "-",
last_activity: item.last_activity ? item.last_activity.last_activity_string : "-",
project_health: item.project_health,
project_update: item.comment ? item.comment : "-",
});
}
// 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 exportProjectTimeLogs(req: IWorkLenzRequest, res: IWorkLenzResponse) {
const result = await this.getProjectTimeLogs(req);
const exportDate = moment().format("MMM-DD-YYYY");
const fileName = `${req.query.project_name} Time Logs - ${exportDate}`;
const workbook = new Excel.Workbook();
const sheet = workbook.addWorksheet("Time Logs");
sheet.columns = [
{ header: "Date", key: "date", width: 30 },
{ header: "Log", key: "log", width: 120 },
];
sheet.getCell("A1").value = `Time Logs from ${req.query.project_name}`;
sheet.mergeCells("A1:O1");
sheet.getCell("A1").alignment = { horizontal: "center" };
sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
sheet.getCell("A1").font = { size: 16 };
sheet.getCell("A2").value = `Exported on : ${exportDate}`;
sheet.mergeCells("A2:O2");
sheet.getCell("A2").alignment = { horizontal: "center" };
sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
sheet.getCell("A2").font = { size: 12 };
sheet.getRow(4).values = ["Date", "Log"];
sheet.getRow(4).font = { bold: true };
for (const row of result) {
for (const log of row.logs) {
sheet.addRow({
date: row.log_day,
log: `${log.user_name} logged ${log.time_spent_string} for ${log.task_name}`
});
}
}
res.setHeader("Content-Type", "application/vnd.openxmlformats");
res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
await workbook.xlsx.write(res)
.then(() => {
res.end();
});
}
private static async getProjectTimeLogs(req: IWorkLenzRequest) {
const projectId = req.query.id;
const duration = req.query.duration as string;
const date_range = req.query.date_range as [];
const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range);
const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range);
const q = `SELECT
(SELECT name FROM projects WHERE projects.id = $1) AS project_name,
(SELECT key FROM projects WHERE projects.id = $1) AS project_key,
(SELECT task_no FROM tasks WHERE tasks.id = task_work_log.task_id) AS task_key_num,
(SELECT name FROM tasks WHERE tasks.id = task_work_log.task_id) AS task_name,
task_work_log.time_spent,
(SELECT name FROM users WHERE users.id = task_work_log.user_id) AS user_name,
(SELECT email FROM users WHERE users.id = task_work_log.user_id) AS user_email,
(SELECT avatar_url FROM users WHERE users.id = task_work_log.user_id) AS avatar_url,
task_work_log.created_at
${minMaxDateClause}
FROM task_work_log
WHERE
task_id IN (select id from tasks WHERE project_id = $1)
${durationClause}
ORDER BY task_work_log.created_at DESC`;
const result = await db.query(q, [projectId]);
const formattedResult = await this.formatLog(result.rows);
const logGroups = await this.getTimeLogDays(formattedResult);
return logGroups;
}
protected static getMinMaxDates(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");
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(task_work_log.created_at)::DATE) FROM task_work_log WHERE task_id IN (SELECT id FROM tasks WHERE project_id = $1)) AS start_date, (SELECT (MAX(task_work_log.created_at)::DATE) FROM task_work_log WHERE task_id IN (SELECT id FROM tasks WHERE project_id = $1)) AS end_date";
return "";
}
private static async formatLog(result: any[]) {
result.forEach((row) => {
const duration = moment.duration(row.time_spent, "seconds");
row.time_spent_string = this.formatDuration(duration);
row.task_key = `${row.project_key}-${row.task_key_num}`;
});
return result;
}
private static async getTimeLogDays(result: any[]) {
if (result.length) {
const startDate = moment(result[0].start_date).isValid() ? moment(result[0].start_date, "YYYY-MM-DD").clone() : null;
const endDate = moment(result[0].end_date).isValid() ? moment(result[0].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.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 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;
}
}

View File

@@ -0,0 +1,502 @@
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 { getColor, int, log_error } from "../../shared/utils";
import ReportingControllerBase from "./reporting-controller-base";
import { DATE_RANGES } from "../../shared/constants";
import Excel from "exceljs";
enum IToggleOptions {
'WORKING_DAYS' = 'WORKING_DAYS', 'MAN_DAYS' = 'MAN_DAYS'
}
export default class ReportingAllocationController extends ReportingControllerBase {
private static async getTimeLoggedByProjects(projects: string[], users: string[], key: string, dateRange: string[], archived = false, user_id = ""): Promise<any> {
try {
const projectIds = projects.map(p => `'${p}'`).join(",");
const userIds = users.map(u => `'${u}'`).join(",");
const duration = this.getDateRangeClause(key || DATE_RANGES.LAST_WEEK, dateRange);
const archivedClause = archived
? ""
: `AND projects.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = projects.id AND user_id = '${user_id}') `;
const projectTimeLogs = await this.getTotalTimeLogsByProject(archived, duration, projectIds, userIds, archivedClause);
const userTimeLogs = await this.getTotalTimeLogsByUser(archived, duration, projectIds, userIds);
const format = (seconds: number) => {
if (seconds === 0) return "-";
const duration = moment.duration(seconds, "seconds");
const formattedDuration = `${~~(duration.asHours())}h ${duration.minutes()}m ${duration.seconds()}s`;
return formattedDuration;
};
let totalProjectsTime = 0;
let totalUsersTime = 0;
for (const project of projectTimeLogs) {
if (project.all_tasks_count > 0) {
project.progress = Math.round((project.completed_tasks_count / project.all_tasks_count) * 100);
} else {
project.progress = 0;
}
let total = 0;
for (const log of project.time_logs) {
total += log.time_logged;
log.time_logged = format(log.time_logged);
}
project.totalProjectsTime = totalProjectsTime + total;
project.total = format(total);
}
for (const log of userTimeLogs) {
log.totalUsersTime = totalUsersTime + parseInt(log.time_logged)
log.time_logged = format(parseInt(log.time_logged));
}
return { projectTimeLogs, userTimeLogs };
} catch (error) {
log_error(error);
}
return [];
}
private static async getTotalTimeLogsByProject(archived: boolean, duration: string, projectIds: string, userIds: string, archivedClause = "") {
try {
const q = `SELECT projects.name,
projects.color_code,
sps.name AS status_name,
sps.color_code AS status_color_code,
sps.icon AS status_icon,
(SELECT COUNT(*)
FROM tasks
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
AND project_id = projects.id) AS all_tasks_count,
(SELECT COUNT(*)
FROM tasks
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
AND project_id = projects.id
AND status_id IN (SELECT id
FROM task_statuses
WHERE project_id = projects.id
AND category_id IN
(SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS completed_tasks_count,
(
SELECT COALESCE(JSON_AGG(r), '[]'::JSON)
FROM (
SELECT name,
(SELECT COALESCE(SUM(time_spent), 0)
FROM task_work_log
LEFT JOIN tasks t ON task_work_log.task_id = t.id
WHERE user_id = users.id
AND CASE WHEN ($1 IS TRUE) THEN t.project_id IS NOT NULL ELSE t.archived = FALSE END
AND t.project_id = projects.id
${duration}) AS time_logged
FROM users
WHERE id IN (${userIds})
ORDER BY name
) r
) AS time_logs
FROM projects
LEFT JOIN sys_project_statuses sps ON projects.status_id = sps.id
WHERE projects.id IN (${projectIds}) ${archivedClause};`;
const result = await db.query(q, [archived]);
return result.rows;
} catch (error) {
log_error(error);
return [];
}
}
private static async getTotalTimeLogsByUser(archived: boolean, duration: string, projectIds: string, userIds: string) {
try {
const q = `(SELECT id,
(SELECT COALESCE(SUM(time_spent), 0)
FROM task_work_log
LEFT JOIN tasks t ON task_work_log.task_id = t.id
WHERE user_id = users.id
AND CASE WHEN ($1 IS TRUE) THEN t.project_id IS NOT NULL ELSE t.archived = FALSE END
AND t.project_id IN (${projectIds})
${duration}) AS time_logged
FROM users
WHERE id IN (${userIds})
ORDER BY name);`;
const result = await db.query(q, [archived]);
return result.rows;
} catch (error) {
log_error(error);
return [];
}
}
private static async getUserIds(teamIds: any) {
try {
const q = `SELECT id, (SELECT name)
FROM users
WHERE id IN (SELECT user_id
FROM team_members
WHERE team_id IN (${teamIds}))
GROUP BY id
ORDER BY name`;
const result = await db.query(q, []);
return result.rows;
} catch (error) {
log_error(error);
return [];
}
}
@HandleExceptions()
public static async getAllocation(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const teams = (req.body.teams || []) as string[]; // ids
const teamIds = teams.map(id => `'${id}'`).join(",");
const projectIds = (req.body.projects || []) as string[];
if (!teamIds || !projectIds.length)
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
const users = await this.getUserIds(teamIds);
const userIds = users.map((u: any) => u.id);
const { projectTimeLogs, userTimeLogs } = await this.getTimeLoggedByProjects(projectIds, userIds, req.body.duration, req.body.date_range, (req.query.archived === "true"), req.user?.id);
for (const [i, user] of users.entries()) {
user.total_time = userTimeLogs[i].time_logged;
}
return res.status(200).send(new ServerResponse(true, { users, projects: projectTimeLogs }));
}
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 teams = (req.query.teams as string)?.split(",");
const teamIds = teams.map(t => `'${t}'`).join(",");
const projectIds = (req.query.projects as string)?.split(",");
const duration = req.query.duration;
const dateRange = (req.query.date_range as string)?.split(",");
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 users = await this.getUserIds(teamIds);
const userIds = users.map((u: any) => u.id);
const { projectTimeLogs, userTimeLogs } = await this.getTimeLoggedByProjects(projectIds, userIds, duration as string, dateRange, (req.query.include_archived === "true"), req.user?.id);
for (const [i, user] of users.entries()) {
user.total_time = userTimeLogs[i].time_logged;
}
// excel file
const exportDate = moment().format("MMM-DD-YYYY");
const fileName = `Reporting Time Sheet - ${exportDate}`;
const workbook = new Excel.Workbook();
const sheet = workbook.addWorksheet("Reporting Time Sheet");
sheet.columns = [
{ header: "Project", key: "project", width: 25 },
{ header: "Logged Time", key: "logged_time", width: 20 },
{ header: "Total", key: "total", width: 25 },
];
sheet.getCell("A1").value = `Reporting Time Sheet`;
sheet.mergeCells("A1:G1");
sheet.getCell("A1").alignment = { horizontal: "center" };
sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
sheet.getCell("A1").font = { size: 16 };
sheet.getCell("A2").value = `Exported on : ${exportDate}`;
sheet.mergeCells("A2:G2");
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");
let totalProjectTime = 0;
let totalMemberTime = 0;
if (projectTimeLogs.length > 0) {
const rowTop = sheet.getRow(5);
rowTop.getCell(1).value = "";
users.forEach((user: { id: string, name: string, total_time: string }, index: any) => {
rowTop.getCell(index + 2).value = user.name;
});
rowTop.getCell(users.length + 2).value = "Total";
rowTop.font = {
bold: true
};
for (const project of projectTimeLogs) {
const rowValues = [];
rowValues[1] = project.name;
project.time_logs.forEach((log: any, index: any) => {
rowValues[index + 2] = log.time_logged === "0h 0m 0s" ? "-" : log.time_logged;
});
rowValues[project.time_logs.length + 2] = project.total;
sheet.addRow(rowValues);
const { lastRow } = sheet;
if (lastRow) {
const totalCell = lastRow.getCell(project.time_logs.length + 2);
totalCell.style.font = { bold: true };
}
totalProjectTime = totalProjectTime + project.totalProjectsTime
}
const rowBottom = sheet.getRow(projectTimeLogs.length + 6);
rowBottom.getCell(1).value = "Total";
rowBottom.getCell(1).style.font = { bold: true };
userTimeLogs.forEach((log: { id: string, time_logged: string, totalUsersTime: number }, index: any) => {
totalMemberTime = totalMemberTime + log.totalUsersTime
rowBottom.getCell(index + 2).value = log.time_logged;
});
rowBottom.font = {
bold: true
};
}
const format = (seconds: number) => {
if (seconds === 0) return "-";
const duration = moment.duration(seconds, "seconds");
const formattedDuration = `${~~(duration.asHours())}h ${duration.minutes()}m ${duration.seconds()}s`;
return formattedDuration;
};
const projectTotalTimeRow = sheet.getRow(projectTimeLogs.length + 8);
projectTotalTimeRow.getCell(1).value = "Total logged time of Projects"
projectTotalTimeRow.getCell(2).value = `${format(totalProjectTime)}`
projectTotalTimeRow.getCell(1).style.font = { bold: true };
projectTotalTimeRow.getCell(2).style.font = { bold: true };
const membersTotalTimeRow = sheet.getRow(projectTimeLogs.length + 9);
membersTotalTimeRow.getCell(1).value = "Total logged time of Members"
membersTotalTimeRow.getCell(2).value = `${format(totalMemberTime)}`
membersTotalTimeRow.getCell(1).style.font = { bold: true };
membersTotalTimeRow.getCell(2).style.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 getProjectTimeSheets(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const archived = req.query.archived === "true";
const teams = (req.body.teams || []) as string[]; // ids
const teamIds = teams.map(id => `'${id}'`).join(",");
const projects = (req.body.projects || []) as string[];
const projectIds = projects.map(p => `'${p}'`).join(",");
if (!teamIds || !projectIds.length)
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
const { duration, date_range } = req.body;
const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range);
const archivedClause = archived
? ""
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
const q = `
SELECT p.id,
p.name,
(SELECT SUM(time_spent)) AS logged_time,
SUM(total_minutes) AS estimated,
color_code
FROM projects p
LEFT JOIN tasks t ON t.project_id = p.id
LEFT JOIN task_work_log ON task_work_log.task_id = t.id
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
GROUP BY p.id, p.name
ORDER BY logged_time DESC;`;
const result = await db.query(q, []);
const data = [];
for (const project of result.rows) {
project.value = project.logged_time ? parseFloat(moment.duration(project.logged_time, "seconds").asHours().toFixed(2)) : 0;
project.estimated_value = project.estimated ? parseFloat(moment.duration(project.estimated, "minutes").asHours().toFixed(2)) : 0;
if (project.value > 0 ) {
data.push(project);
}
}
return res.status(200).send(new ServerResponse(true, data));
}
@HandleExceptions()
public static async getMemberTimeSheets(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const archived = req.query.archived === "true";
const teams = (req.body.teams || []) as string[]; // ids
const teamIds = teams.map(id => `'${id}'`).join(",");
const projects = (req.body.projects || []) as string[];
const projectIds = projects.map(p => `'${p}'`).join(",");
if (!teamIds || !projectIds.length)
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
const { duration, date_range } = req.body;
const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range);
const archivedClause = archived
? ""
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
const q = `
SELECT tmiv.email, tmiv.name, SUM(time_spent) AS logged_time
FROM team_member_info_view tmiv
LEFT JOIN task_work_log ON task_work_log.user_id = tmiv.user_id
LEFT JOIN tasks t ON t.id = task_work_log.task_id
LEFT JOIN projects p ON p.id = t.project_id AND p.team_id = tmiv.team_id
WHERE p.id IN (${projectIds})
${durationClause} ${archivedClause}
GROUP BY tmiv.email, tmiv.name
ORDER BY logged_time DESC;`;
const result = await db.query(q, []);
for (const member of result.rows) {
member.value = member.logged_time ? parseFloat(moment.duration(member.logged_time, "seconds").asHours().toFixed(2)) : 0;
member.color_code = getColor(member.name);
}
return res.status(200).send(new ServerResponse(true, result.rows));
}
private static getEstimated(project: any, type: string) {
switch (type) {
case IToggleOptions.MAN_DAYS:
return project.estimated_man_days ?? 0;;
case IToggleOptions.WORKING_DAYS:
return project.estimated_working_days ?? 0;;
default:
return 0;
}
}
@HandleExceptions()
public static async getEstimatedVsActual(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const archived = req.query.archived === "true";
const teams = (req.body.teams || []) as string[]; // ids
const teamIds = teams.map(id => `'${id}'`).join(",");
const projects = (req.body.projects || []) as string[];
const projectIds = projects.map(p => `'${p}'`).join(",");
const { type } = req.body;
if (!teamIds || !projectIds.length)
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
const { duration, date_range } = req.body;
const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range);
const archivedClause = archived
? ""
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
const q = `
SELECT p.id,
p.name,
p.end_date,
p.hours_per_day::INT,
p.estimated_man_days::INT,
p.estimated_working_days::INT,
(SELECT SUM(time_spent)) AS logged_time,
(SELECT COALESCE(SUM(total_minutes), 0)
FROM tasks
WHERE project_id = p.id) AS estimated,
color_code
FROM projects p
LEFT JOIN tasks t ON t.project_id = p.id
LEFT JOIN task_work_log ON task_work_log.task_id = t.id
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
GROUP BY p.id, p.name
ORDER BY logged_time DESC;`;
const result = await db.query(q, []);
const data = [];
for (const project of result.rows) {
const durationInHours = parseFloat(moment.duration(project.logged_time, "seconds").asHours().toFixed(2));
const hoursPerDay = parseInt(project.hours_per_day ?? 1);
project.value = parseFloat((durationInHours / hoursPerDay).toFixed(2)) ?? 0;
project.estimated_value = this.getEstimated(project, type);
project.estimated_man_days = project.estimated_man_days ?? 0;
project.estimated_working_days = project.estimated_working_days ?? 0;
project.hours_per_day = project.hours_per_day ?? 0;
if (project.value > 0 || project.estimated_value > 0 ) {
data.push(project);
}
}
return res.status(200).send(new ServerResponse(true, data));
}
}

View File

@@ -0,0 +1,604 @@
import WorklenzControllerBase from "../worklenz-controller-base";
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
import db from "../../config/db";
import moment from "moment";
import { DATE_RANGES, TASK_PRIORITY_COLOR_ALPHA } from "../../shared/constants";
import { formatDuration, formatLogText, getColor, int } from "../../shared/utils";
export default abstract class ReportingControllerBase extends WorklenzControllerBase {
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;
}
protected static async getTotalTasksCount(projectId: string | null) {
const q = `
SELECT COUNT(*) AS count
FROM tasks
WHERE project_id = $1;
`;
const result = await db.query(q, [projectId]);
const [data] = result.rows;
return data.count || 0;
}
protected static async getArchivedProjectsClause(archived = false, user_id: string, column_name: string) {
return archived
? ""
: `AND ${column_name} NOT IN (SELECT project_id FROM archived_projects WHERE project_id = ${column_name} AND user_id = '${user_id}') `;
}
protected static async getAllTasks(projectId: string | null) {
const q = `
SELECT id,
name,
parent_task_id,
parent_task_id IS NOT NULL AS is_sub_task,
status_id AS status,
(SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status_name,
(SELECT color_code
FROM sys_task_status_categories
WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color,
priority_id AS priority,
(SELECT value FROM task_priorities WHERE id = tasks.priority_id) AS priority_value,
(SELECT name FROM task_priorities WHERE id = tasks.priority_id) AS priority_name,
(SELECT color_code FROM task_priorities WHERE id = tasks.priority_id) AS priority_color,
end_date,
(SELECT phase_id FROM task_phase WHERE task_id = tasks.id) AS phase_id,
(SELECT name
FROM project_phases
WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = tasks.id)) AS phase_name,
completed_at,
total_minutes,
(SELECT SUM(time_spent) FROM task_work_log WHERE task_id = tasks.id) AS total_seconds_spent
FROM tasks
WHERE project_id = $1
ORDER BY name;
`;
const result = await db.query(q, [projectId]);
for (const item of result.rows) {
const endDate = moment(item.end_date);
const completedDate = moment(item.completed_at);
const overdueDays = completedDate.diff(endDate, "days");
if (overdueDays > 0) {
item.overdue_days = overdueDays.toString();
} else {
item.overdue_days = "0";
}
item.total_minutes_spent = Math.ceil(item.total_seconds_spent / 60);
if (~~(item.total_minutes_spent) > ~~(item.total_minutes)) {
const overlogged_time = ~~(item.total_minutes_spent) - ~~(item.total_minutes);
item.overlogged_time_string = formatDuration(moment.duration(overlogged_time, "minutes"));
} else {
item.overlogged_time_string = `0h 0m`;
}
}
return result.rows;
}
protected static getDateRangeClause(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");
let query = `AND task_work_log.created_at::DATE >= '${start}'::DATE AND task_work_log.created_at < '${end}'::DATE + INTERVAL '1 day'`;
if (start === end) {
query = `AND task_work_log.created_at::DATE = '${start}'::DATE`;
}
return query;
}
if (key === DATE_RANGES.YESTERDAY)
return "AND task_work_log.created_at >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE";
if (key === DATE_RANGES.LAST_WEEK)
return "AND task_work_log.created_at >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'";
if (key === DATE_RANGES.LAST_MONTH)
return "AND task_work_log.created_at >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'";
if (key === DATE_RANGES.LAST_QUARTER)
return "AND task_work_log.created_at >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'";
return "";
}
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);
}
public static async getProjectsByTeam(
teamId: string,
size: string | number | null,
offset: string | number | null,
searchQuery: string | null,
sortField: string,
sortOrder: string,
statusClause: string,
healthClause: string,
categoryClause: string,
archivedClause = "",
teamFilterClause: string,
projectManagersClause: string) {
const q = `SELECT COUNT(*) AS total,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
FROM (SELECT p.id,
p.name,
p.color_code,
p.health_id AS project_health,
(SELECT color_code
FROM sys_project_healths
WHERE sys_project_healths.id = p.health_id) AS health_color,
pc.id AS category_id,
pc.name AS category_name,
pc.color_code AS category_color,
(SELECT name FROM clients WHERE id = p.client_id) AS client,
p.team_id,
(SELECT name FROM teams WHERE id = p.team_id) AS team_name,
ps.id AS status_id,
ps.name AS status_name,
ps.color_code AS status_color,
ps.icon AS status_icon,
start_date,
end_date,
(SELECT COALESCE(ROW_TO_JSON(pm), '{}'::JSON)
FROM (SELECT team_member_id AS 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
AND tmiv.team_id = (SELECT team_id FROM projects WHERE id = p.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 =p.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 = p.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 = p.id) AS actual_time,
(SELECT ROW_TO_JSON(rec)
FROM (SELECT COUNT(ta.id) AS total,
COUNT(CASE WHEN is_completed(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS done,
COUNT(CASE WHEN is_doing(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS doing,
COUNT(CASE WHEN is_todo(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS todo
FROM tasks ta
WHERE project_id = p.id) rec) AS tasks_stat,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
FROM (SELECT pu.content AS content,
(SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
FROM (SELECT u.name AS user_name,
u.email AS user_email
FROM project_comment_mentions pcm
LEFT JOIN users u ON pcm.informed_by = u.id
WHERE pcm.comment_id = pu.id) rec) AS mentions,
pu.updated_at
FROM project_comments pu
WHERE pu.project_id = p.id
ORDER BY pu.updated_at DESC
LIMIT 1) AS rec) AS update,
(SELECT ROW_TO_JSON(rec)
FROM (SELECT attribute_type,
log_type,
-- new case,
(CASE
WHEN (attribute_type = 'status')
THEN (SELECT name FROM task_statuses WHERE id = old_value::UUID)
WHEN (attribute_type = 'priority')
THEN (SELECT name FROM task_priorities WHERE id = old_value::UUID)
ELSE (old_value) END) AS previous,
-- new case
(CASE
WHEN (attribute_type = 'assignee')
THEN (SELECT name FROM users WHERE id = new_value::UUID)
WHEN (attribute_type = 'label')
THEN (SELECT name FROM team_labels WHERE id = new_value::UUID)
WHEN (attribute_type = 'status')
THEN (SELECT name FROM task_statuses WHERE id = new_value::UUID)
WHEN (attribute_type = 'priority')
THEN (SELECT name FROM task_priorities WHERE id = new_value::UUID)
ELSE (new_value) END) AS current,
(SELECT name
FROM users
WHERE id = (SELECT reporter_id FROM tasks WHERE id = tal.task_id)),
(SELECT ROW_TO_JSON(rec)
FROM (SELECT (SELECT name FROM users WHERE users.id = tal.user_id),
(SELECT avatar_url FROM users WHERE users.id = tal.user_id)) rec) AS done_by,
(CASE
WHEN (attribute_type = 'assignee')
THEN (SELECT ROW_TO_JSON(rec)
FROM (SELECT (CASE
WHEN (new_value IS NOT NULL)
THEN (SELECT name FROM users WHERE users.id = new_value::UUID)
ELSE (next_string) END) AS name,
(SELECT avatar_url FROM users WHERE users.id = new_value::UUID)) rec)
ELSE (NULL) END) AS assigned_user,
(SELECT name FROM tasks WHERE tasks.id = tal.task_id)
FROM task_activity_logs tal
WHERE task_id IN (SELECT id FROM tasks t WHERE t.project_id = p.id)
ORDER BY tal.created_at DESC
LIMIT 1) rec) AS last_activity
FROM projects p
LEFT JOIN project_categories pc ON pc.id = p.category_id
LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
WHERE ${teamFilterClause} ${searchQuery} ${healthClause} ${statusClause} ${categoryClause} ${projectManagersClause} ${archivedClause}
ORDER BY ${sortField} ${sortOrder}
LIMIT $2 OFFSET $3) t) AS projects
FROM projects p
LEFT JOIN project_categories pc ON pc.id = p.category_id
LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
WHERE ${teamFilterClause} ${searchQuery} ${healthClause} ${statusClause} ${categoryClause} ${projectManagersClause} ${archivedClause};`;
const result = await db.query(q, [teamId, size, offset]);
const [data] = result.rows;
for (const project of data.projects) {
if (project.project_manager) {
project.project_manager.name = project.project_manager.project_manager_info.name;
project.project_manager.avatar_url = project.project_manager.project_manager_info.avatar_url;
project.project_manager.color_code = getColor(project.project_manager.name);
}
}
return data;
}
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`;
}
public static async exportProjects(teamId: string) {
const q = `SELECT COUNT(*) AS total,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
FROM (SELECT p.id,
p.name,
(SELECT name
FROM sys_project_healths
WHERE sys_project_healths.id = p.health_id) AS project_health,
pc.name AS category_name,
(SELECT name FROM clients WHERE id = p.client_id) AS client,
(SELECT name FROM teams WHERE id = p.team_id) AS team_name,
ps.name AS status_name,
start_date,
end_date,
(SELECT COALESCE(SUM(total_minutes), 0)
FROM tasks
WHERE project_id = p.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 = p.id) AS actual_time,
(SELECT ROW_TO_JSON(rec)
FROM (SELECT COUNT(ta.id) AS total,
COUNT(CASE WHEN is_completed(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS done,
COUNT(CASE WHEN is_doing(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS doing,
COUNT(CASE WHEN is_todo(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS todo
FROM tasks ta
WHERE project_id = p.id) rec) AS tasks_stat,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
FROM (SELECT pu.content AS content,
(SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
FROM (SELECT u.name AS user_name,
u.email AS user_email
FROM project_comment_mentions pcm
LEFT JOIN users u ON pcm.informed_by = u.id
WHERE pcm.comment_id = pu.id) rec) AS mentions,
pu.updated_at
FROM project_comments pu
WHERE pu.project_id = p.id
ORDER BY pu.updated_at DESC
LIMIT 1) AS rec) AS update,
(SELECT ROW_TO_JSON(rec)
FROM (SELECT attribute_type,
log_type,
-- new case,
(CASE
WHEN (attribute_type = 'status')
THEN (SELECT name FROM task_statuses WHERE id = old_value::UUID)
WHEN (attribute_type = 'priority')
THEN (SELECT name FROM task_priorities WHERE id = old_value::UUID)
ELSE (old_value) END) AS previous,
-- new case
(CASE
WHEN (attribute_type = 'assignee')
THEN (SELECT name FROM users WHERE id = new_value::UUID)
WHEN (attribute_type = 'label')
THEN (SELECT name FROM team_labels WHERE id = new_value::UUID)
WHEN (attribute_type = 'status')
THEN (SELECT name FROM task_statuses WHERE id = new_value::UUID)
WHEN (attribute_type = 'priority')
THEN (SELECT name FROM task_priorities WHERE id = new_value::UUID)
ELSE (new_value) END) AS current,
(SELECT name
FROM users
WHERE id = (SELECT reporter_id FROM tasks WHERE id = tal.task_id)),
(SELECT ROW_TO_JSON(rec)
FROM (SELECT (SELECT name FROM users WHERE users.id = tal.user_id),
(SELECT avatar_url FROM users WHERE users.id = tal.user_id)) rec) AS done_by,
(CASE
WHEN (attribute_type = 'assignee')
THEN (SELECT ROW_TO_JSON(rec)
FROM (SELECT (CASE
WHEN (new_value IS NOT NULL)
THEN (SELECT name FROM users WHERE users.id = new_value::UUID)
ELSE (next_string) END) AS name,
(SELECT avatar_url FROM users WHERE users.id = new_value::UUID)) rec)
ELSE (NULL) END) AS assigned_user,
(SELECT name FROM tasks WHERE tasks.id = tal.task_id)
FROM task_activity_logs tal
WHERE task_id IN (SELECT id FROM tasks t WHERE t.project_id = p.id)
ORDER BY tal.created_at
LIMIT 1) rec) AS last_activity
FROM projects p
LEFT JOIN project_categories pc ON pc.id = p.category_id
LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
WHERE p.team_id = $1 ORDER BY p.name) t) AS projects
FROM projects p
LEFT JOIN project_categories pc ON pc.id = p.category_id
LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
WHERE p.team_id = $1;`;
const result = await db.query(q, [teamId]);
const [data] = result.rows;
for (const project of data.projects) {
project.team_color = getColor(project.team_name) + TASK_PRIORITY_COLOR_ALPHA;
project.days_left = this.getDaysLeft(project.end_date);
project.is_overdue = this.isOverdue(project.end_date);
if (project.days_left && project.is_overdue) {
project.days_left = project.days_left.toString().replace(/-/g, "");
}
project.is_today = this.isToday(project.end_date);
project.estimated_time = this.convertMinutesToHoursAndMinutes(int(project.estimated_time));
project.actual_time = this.convertSecondsToHoursAndMinutes(int(project.actual_time));
project.tasks_stat = {
todo: this.getPercentage(int(project.tasks_stat.todo), +project.tasks_stat.total),
doing: this.getPercentage(int(project.tasks_stat.doing), +project.tasks_stat.total),
done: this.getPercentage(int(project.tasks_stat.done), +project.tasks_stat.total)
};
if (project.update.length > 0) {
const update = project.update[0];
const placeHolders = update.content.match(/{\d+}/g);
if (placeHolders) {
placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
const index = parseInt(placeHolder.match(/\d+/)[0]);
if (index >= 0 && index < update.mentions.length) {
update.content = update.content.replace(placeHolder, ` @${update.mentions[index].user_name} `);
}
});
}
project.comment = update.content;
}
if (project.last_activity) {
if (project.last_activity.attribute_type === "estimation") {
project.last_activity.previous = formatDuration(moment.duration(project.last_activity.previous, "minutes"));
project.last_activity.current = formatDuration(moment.duration(project.last_activity.current, "minutes"));
}
if (project.last_activity.assigned_user) project.last_activity.assigned_user.color_code = getColor(project.last_activity.assigned_user.name);
project.last_activity.done_by.color_code = getColor(project.last_activity.done_by.name);
project.last_activity.log_text = await formatLogText(project.last_activity);
project.last_activity.attribute_type = project.last_activity.attribute_type?.replace(/_/g, " ");
project.last_activity.last_activity_string = `${project.last_activity.done_by.name} ${project.last_activity.log_text} ${project.last_activity.attribute_type}`;
}
}
return data;
}
public static async exportProjectsAll(teamId: string) {
const q = `SELECT COUNT(*) AS total,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
FROM (SELECT p.id,
p.name,
(SELECT name
FROM sys_project_healths
WHERE sys_project_healths.id = p.health_id) AS project_health,
pc.name AS category_name,
(SELECT name FROM clients WHERE id = p.client_id) AS client,
(SELECT name FROM teams WHERE id = p.team_id) AS team_name,
ps.name AS status_name,
start_date,
end_date,
(SELECT COALESCE(SUM(total_minutes), 0)
FROM tasks
WHERE project_id = p.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 = p.id) AS actual_time,
(SELECT ROW_TO_JSON(rec)
FROM (SELECT COUNT(ta.id) AS total,
COUNT(CASE WHEN is_completed(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS done,
COUNT(CASE WHEN is_doing(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS doing,
COUNT(CASE WHEN is_todo(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS todo
FROM tasks ta
WHERE project_id = p.id) rec) AS tasks_stat,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
FROM (SELECT pu.content AS content,
(SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
FROM (SELECT u.name AS user_name,
u.email AS user_email
FROM project_comment_mentions pcm
LEFT JOIN users u ON pcm.informed_by = u.id
WHERE pcm.comment_id = pu.id) rec) AS mentions,
pu.updated_at
FROM project_comments pu
WHERE pu.project_id = p.id
ORDER BY pu.updated_at DESC
LIMIT 1) AS rec) AS update,
(SELECT ROW_TO_JSON(rec)
FROM (SELECT attribute_type,
log_type,
-- new case,
(CASE
WHEN (attribute_type = 'status')
THEN (SELECT name FROM task_statuses WHERE id = old_value::UUID)
WHEN (attribute_type = 'priority')
THEN (SELECT name FROM task_priorities WHERE id = old_value::UUID)
ELSE (old_value) END) AS previous,
-- new case
(CASE
WHEN (attribute_type = 'assignee')
THEN (SELECT name FROM users WHERE id = new_value::UUID)
WHEN (attribute_type = 'label')
THEN (SELECT name FROM team_labels WHERE id = new_value::UUID)
WHEN (attribute_type = 'status')
THEN (SELECT name FROM task_statuses WHERE id = new_value::UUID)
WHEN (attribute_type = 'priority')
THEN (SELECT name FROM task_priorities WHERE id = new_value::UUID)
ELSE (new_value) END) AS current,
(SELECT name
FROM users
WHERE id = (SELECT reporter_id FROM tasks WHERE id = tal.task_id)),
(SELECT ROW_TO_JSON(rec)
FROM (SELECT (SELECT name FROM users WHERE users.id = tal.user_id),
(SELECT avatar_url FROM users WHERE users.id = tal.user_id)) rec) AS done_by,
(CASE
WHEN (attribute_type = 'assignee')
THEN (SELECT ROW_TO_JSON(rec)
FROM (SELECT (CASE
WHEN (new_value IS NOT NULL)
THEN (SELECT name FROM users WHERE users.id = new_value::UUID)
ELSE (next_string) END) AS name,
(SELECT avatar_url FROM users WHERE users.id = new_value::UUID)) rec)
ELSE (NULL) END) AS assigned_user,
(SELECT name FROM tasks WHERE tasks.id = tal.task_id)
FROM task_activity_logs tal
WHERE task_id IN (SELECT id FROM tasks t WHERE t.project_id = p.id)
ORDER BY tal.created_at
LIMIT 1) rec) AS last_activity
FROM projects p
LEFT JOIN project_categories pc ON pc.id = p.category_id
LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
WHERE in_organization(p.team_id, $1) ORDER BY p.name) t) AS projects
FROM projects p
LEFT JOIN project_categories pc ON pc.id = p.category_id
LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
WHERE in_organization(p.team_id, $1);`;
const result = await db.query(q, [teamId]);
const [data] = result.rows;
for (const project of data.projects) {
project.team_color = getColor(project.team_name) + TASK_PRIORITY_COLOR_ALPHA;
project.days_left = this.getDaysLeft(project.end_date);
project.is_overdue = this.isOverdue(project.end_date);
if (project.days_left && project.is_overdue) {
project.days_left = project.days_left.toString().replace(/-/g, "");
}
project.is_today = this.isToday(project.end_date);
project.estimated_time = this.convertMinutesToHoursAndMinutes(int(project.estimated_time));
project.actual_time = this.convertSecondsToHoursAndMinutes(int(project.actual_time));
project.tasks_stat = {
todo: this.getPercentage(int(project.tasks_stat.todo), +project.tasks_stat.total),
doing: this.getPercentage(int(project.tasks_stat.doing), +project.tasks_stat.total),
done: this.getPercentage(int(project.tasks_stat.done), +project.tasks_stat.total)
};
if (project.update.length > 0) {
const update = project.update[0];
const placeHolders = update.content.match(/{\d+}/g);
if (placeHolders) {
placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
const index = parseInt(placeHolder.match(/\d+/)[0]);
if (index >= 0 && index < update.mentions.length) {
update.content = update.content.replace(placeHolder, ` @${update.mentions[index].user_name} `);
}
});
}
project.comment = update.content;
}
if (project.last_activity) {
if (project.last_activity.attribute_type === "estimation") {
project.last_activity.previous = formatDuration(moment.duration(project.last_activity.previous, "minutes"));
project.last_activity.current = formatDuration(moment.duration(project.last_activity.current, "minutes"));
}
if (project.last_activity.assigned_user) project.last_activity.assigned_user.color_code = getColor(project.last_activity.assigned_user.name);
project.last_activity.done_by.color_code = getColor(project.last_activity.done_by.name);
project.last_activity.log_text = await formatLogText(project.last_activity);
project.last_activity.attribute_type = project.last_activity.attribute_type?.replace(/_/g, " ");
project.last_activity.last_activity_string = `${project.last_activity.done_by.name} ${project.last_activity.log_text} ${project.last_activity.attribute_type}`;
}
}
return data;
}
}

View File

@@ -0,0 +1,20 @@
import ReportingControllerBase from "./reporting-controller-base";
import HandleExceptions from "../../decorators/handle-exceptions";
import {IWorkLenzRequest} from "../../interfaces/worklenz-request";
import {IWorkLenzResponse} from "../../interfaces/worklenz-response";
import db from "../../config/db";
import {ServerResponse} from "../../models/server-response";
export default class ReportingInfoController extends ReportingControllerBase {
@HandleExceptions()
public static async getInfo(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const q = `
SELECT organization_name
FROM organizations
WHERE user_id = (SELECT user_id FROM teams WHERE id = $1);
`;
const result = await db.query(q, [this.getCurrentTeamId(req)]);
const [data] = result.rows;
return res.status(200).send(new ServerResponse(true, data));
}
}

File diff suppressed because it is too large Load Diff