Initial commit: Angular frontend and Expressjs backend
This commit is contained in:
60
worklenz-backend/src/controllers/reporting/interfaces.ts
Normal file
60
worklenz-backend/src/controllers/reporting/interfaces.ts
Normal 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;
|
||||
}
|
||||
@@ -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)
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import ReportingControllerBase from "../reporting-controller-base";
|
||||
|
||||
export default class ReportingProjectsBase extends ReportingControllerBase {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user