This commit is contained in:
chamikaJ
2025-04-17 18:28:54 +05:30
parent f583291d8a
commit 8825b0410a
2837 changed files with 241385 additions and 127578 deletions

View File

@@ -1,4 +1,4 @@
import { IChartObject } from "./overview/reporting-overview-base";
import * as Highcharts from "highcharts";
export interface IDuration {
label: string;
@@ -34,7 +34,7 @@ export interface IOverviewStatistics {
}
export interface IChartData {
chart: IChartObject[];
chart: Highcharts.PointOptionsObject[];
}
export interface ITasksByStatus extends IChartData {

View File

@@ -1,4 +1,5 @@
import db from "../../../config/db";
import * as Highcharts from "highcharts";
import { ITasksByDue, ITasksByPriority, ITasksByStatus } from "../interfaces";
import ReportingControllerBase from "../reporting-controller-base";
import {
@@ -15,36 +16,33 @@ import {
TASK_STATUS_TODO_COLOR
} from "../../../shared/constants";
import { formatDuration, int } from "../../../shared/utils";
import PointOptionsObject from "../point-options-object";
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;
`;
WITH team_count AS (
SELECT COUNT(*) AS count
FROM teams
WHERE in_organization(id, $1)
),
project_count AS (
SELECT COUNT(*) AS count
FROM projects
WHERE in_organization(team_id, $1) ${archivedQuery}
),
team_member_count AS (
SELECT COUNT(DISTINCT email) AS count
FROM team_member_info_view
WHERE in_organization(team_id, $1)
)
SELECT JSON_BUILD_OBJECT(
'teams', (SELECT count FROM team_count),
'projects', (SELECT count FROM project_count),
'team_members', (SELECT count FROM team_member_count)
) AS counts;`;
const res = await db.query(q, [teamId]);
const [data] = res.rows;
@@ -173,7 +171,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
const doing = int(data?.counts.doing);
const done = int(data?.counts.done);
const chart: IChartObject[] = [];
const chart: Highcharts.PointOptionsObject[] = [];
return {
all,
@@ -209,7 +207,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
const medium = int(data?.counts.medium);
const high = int(data?.counts.high);
const chart: IChartObject[] = [];
const chart: Highcharts.PointOptionsObject[] = [];
return {
all: 0,
@@ -237,7 +235,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
const res = await db.query(q, [projectId]);
const [data] = res.rows;
const chart: IChartObject[] = [];
const chart: Highcharts.PointOptionsObject[] = [];
return {
all: 0,
@@ -251,26 +249,26 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
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),
new PointOptionsObject("Todo", TASK_STATUS_TODO_COLOR, body.todo),
new PointOptionsObject("Doing", TASK_STATUS_DOING_COLOR, body.doing),
new PointOptionsObject("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),
new PointOptionsObject("Low", TASK_PRIORITY_LOW_COLOR, body.low),
new PointOptionsObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, body.medium),
new PointOptionsObject("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),
new PointOptionsObject("Completed", TASK_DUE_COMPLETED_COLOR, body.completed),
new PointOptionsObject("Upcoming", TASK_DUE_UPCOMING_COLOR, body.upcoming),
new PointOptionsObject("Overdue", TASK_DUE_OVERDUE_COLOR, body.overdue),
new PointOptionsObject("No due date", TASK_DUE_NO_DUE_COLOR, body.no_due),
];
}
@@ -581,7 +579,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
`;
const result = await db.query(q, [teamMemberId]);
const chart: IChartObject[] = [];
const chart: Highcharts.PointOptionsObject[] = [];
const total = result.rows.reduce((accumulator: number, current: {
count: number
@@ -589,7 +587,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
for (const project of result.rows) {
project.count = int(project.count);
chart.push(this.createChartObject(project.label, project.color, project.count));
chart.push(new PointOptionsObject(project.label, project.color, project.count));
}
return { chart, total, data: result.rows };
@@ -635,7 +633,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
`;
const result = await db.query(q, [teamMemberId]);
const chart: IChartObject[] = [];
const chart: Highcharts.PointOptionsObject[] = [];
const total = result.rows.reduce((accumulator: number, current: {
count: number
@@ -643,7 +641,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
for (const project of result.rows) {
project.count = int(project.count);
chart.push(this.createChartObject(project.label, project.color, project.count));
chart.push(new PointOptionsObject(project.label, project.color, project.count));
}
return { chart, total, data: result.rows };
@@ -673,10 +671,10 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
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 chart: Highcharts.PointOptionsObject[] = [
new PointOptionsObject("Low", TASK_PRIORITY_LOW_COLOR, d.low),
new PointOptionsObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, d.medium),
new PointOptionsObject("High", TASK_PRIORITY_HIGH_COLOR, d.high),
];
const data = [
@@ -730,10 +728,10 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
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 chart: Highcharts.PointOptionsObject[] = [
new PointOptionsObject("Low", TASK_PRIORITY_LOW_COLOR, d.low),
new PointOptionsObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, d.medium),
new PointOptionsObject("High", TASK_PRIORITY_HIGH_COLOR, d.high),
];
const data = [
@@ -784,10 +782,10 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
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 chart: Highcharts.PointOptionsObject[] = [
new PointOptionsObject("Todo", TASK_STATUS_TODO_COLOR, d.todo),
new PointOptionsObject("Doing", TASK_STATUS_DOING_COLOR, d.doing),
new PointOptionsObject("Done", TASK_STATUS_DONE_COLOR, d.done),
];
const data = [
@@ -826,10 +824,10 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
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 chart: Highcharts.PointOptionsObject[] = [
new PointOptionsObject("Todo", TASK_STATUS_TODO_COLOR, d.todo),
new PointOptionsObject("Doing", TASK_STATUS_DOING_COLOR, d.doing),
new PointOptionsObject("Done", TASK_STATUS_DONE_COLOR, d.done),
];
const data = [
@@ -878,7 +876,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
const in_progress = int(data?.counts.in_progress);
const completed = int(data?.counts.completed);
const chart : IChartObject[] = [];
const chart: Highcharts.PointOptionsObject[] = [];
return {
all,
@@ -908,7 +906,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
`;
const result = await db.query(q, [teamId]);
const chart: IChartObject[] = [];
const chart: Highcharts.PointOptionsObject[] = [];
const total = result.rows.reduce((accumulator: number, current: {
count: number
@@ -916,11 +914,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
for (const category of result.rows) {
category.count = int(category.count);
chart.push({
name: category.label,
color: category.color,
y: category.count
});
chart.push(new PointOptionsObject(category.label, category.color, category.count));
}
return { chart, total, data: result.rows };
@@ -956,7 +950,7 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
const at_risk = int(data?.counts.at_risk);
const good = int(data?.counts.good);
const chart: IChartObject[] = [];
const chart: Highcharts.PointOptionsObject[] = [];
return {
not_set,
@@ -971,22 +965,22 @@ export default class ReportingOverviewBase extends ReportingControllerBase {
// 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)
new PointOptionsObject("Cancelled", "#f37070", body.cancelled),
new PointOptionsObject("Blocked", "#cbc8a1", body.blocked),
new PointOptionsObject("On Hold", "#cbc8a1", body.on_hold),
new PointOptionsObject("Proposed", "#cbc8a1", body.proposed),
new PointOptionsObject("In Planning", "#cbc8a1", body.in_planning),
new PointOptionsObject("In Progress", "#80ca79", body.in_progress),
new PointOptionsObject("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)
new PointOptionsObject("Not Set", "#a9a9a9", body.not_set),
new PointOptionsObject("Needs Attention", "#f37070", body.needs_attention),
new PointOptionsObject("At Risk", "#fbc84c", body.at_risk),
new PointOptionsObject("Good", "#75c997", body.good)
];
}

View File

@@ -0,0 +1,13 @@
import * as Highcharts from "highcharts";
export default class PointOptionsObject implements Highcharts.PointOptionsObject {
name!: string;
color!: string;
y!: number;
constructor(name: string, color: string, y: number) {
this.name = name;
this.color = color;
this.y = y;
}
}

View File

@@ -8,13 +8,14 @@ import { getColor, int, log_error } from "../../shared/utils";
import ReportingControllerBase from "./reporting-controller-base";
import { DATE_RANGES } from "../../shared/constants";
import Excel from "exceljs";
import ChartJsImage from "chartjs-to-image";
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> {
private static async getTimeLoggedByProjects(projects: string[], users: string[], key: string, dateRange: string[], archived = false, user_id = "", billable: { billable: boolean; nonBillable: boolean }): Promise<any> {
try {
const projectIds = projects.map(p => `'${p}'`).join(",");
const userIds = users.map(u => `'${u}'`).join(",");
@@ -24,8 +25,10 @@ export default class ReportingAllocationController extends ReportingControllerBa
? ""
: `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 billableQuery = this.buildBillableQuery(billable);
const projectTimeLogs = await this.getTotalTimeLogsByProject(archived, duration, projectIds, userIds, archivedClause, billableQuery);
const userTimeLogs = await this.getTotalTimeLogsByUser(archived, duration, projectIds, userIds, billableQuery);
const format = (seconds: number) => {
if (seconds === 0) return "-";
@@ -65,7 +68,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
return [];
}
private static async getTotalTimeLogsByProject(archived: boolean, duration: string, projectIds: string, userIds: string, archivedClause = "") {
private static async getTotalTimeLogsByProject(archived: boolean, duration: string, projectIds: string, userIds: string, archivedClause = "", billableQuery = '') {
try {
const q = `SELECT projects.name,
projects.color_code,
@@ -74,12 +77,12 @@ export default class ReportingAllocationController extends ReportingControllerBa
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
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END ${billableQuery}
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 project_id = projects.id ${billableQuery}
AND status_id IN (SELECT id
FROM task_statuses
WHERE project_id = projects.id
@@ -91,10 +94,10 @@ export default class ReportingAllocationController extends ReportingControllerBa
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
LEFT JOIN tasks ON task_work_log.task_id = tasks.id
WHERE user_id = users.id ${billableQuery}
AND CASE WHEN ($1 IS TRUE) THEN tasks.project_id IS NOT NULL ELSE tasks.archived = FALSE END
AND tasks.project_id = projects.id
${duration}) AS time_logged
FROM users
WHERE id IN (${userIds})
@@ -113,15 +116,15 @@ export default class ReportingAllocationController extends ReportingControllerBa
}
}
private static async getTotalTimeLogsByUser(archived: boolean, duration: string, projectIds: string, userIds: string) {
private static async getTotalTimeLogsByUser(archived: boolean, duration: string, projectIds: string, userIds: string, billableQuery = "") {
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
LEFT JOIN tasks ON task_work_log.task_id = tasks.id ${billableQuery}
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})
AND CASE WHEN ($1 IS TRUE) THEN tasks.project_id IS NOT NULL ELSE tasks.archived = FALSE END
AND tasks.project_id IN (${projectIds})
${duration}) AS time_logged
FROM users
WHERE id IN (${userIds})
@@ -154,6 +157,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
@HandleExceptions()
public static async getAllocation(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const teams = (req.body.teams || []) as string[]; // ids
const billable = req.body.billable;
const teamIds = teams.map(id => `'${id}'`).join(",");
const projectIds = (req.body.projects || []) as string[];
@@ -164,7 +168,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
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);
const { projectTimeLogs, userTimeLogs } = await this.getTimeLoggedByProjects(projectIds, userIds, req.body.duration, req.body.date_range, (req.query.archived === "true"), req.user?.id, billable);
for (const [i, user] of users.entries()) {
user.total_time = userTimeLogs[i].time_logged;
@@ -184,6 +188,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
public static async export(req: IWorkLenzRequest, res: IWorkLenzResponse) {
const teams = (req.query.teams as string)?.split(",");
const teamIds = teams.map(t => `'${t}'`).join(",");
const billable = req.body.billable ? req.body.billable : { billable: req.query.billable === "true", nonBillable: req.query.nonBillable === "true" };
const projectIds = (req.query.projects as string)?.split(",");
@@ -218,7 +223,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
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);
const { projectTimeLogs, userTimeLogs } = await this.getTimeLoggedByProjects(projectIds, userIds, duration as string, dateRange, (req.query.include_archived === "true"), req.user?.id, billable);
for (const [i, user] of users.entries()) {
user.total_time = userTimeLogs[i].time_logged;
@@ -341,6 +346,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
const projects = (req.body.projects || []) as string[];
const projectIds = projects.map(p => `'${p}'`).join(",");
const billable = req.body.billable;
if (!teamIds || !projectIds.length)
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
@@ -352,6 +359,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
? ""
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
const billableQuery = this.buildBillableQuery(billable);
const q = `
SELECT p.id,
p.name,
@@ -359,8 +368,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
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
LEFT JOIN tasks ON tasks.project_id = p.id ${billableQuery}
LEFT JOIN task_work_log ON task_work_log.task_id = tasks.id
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
GROUP BY p.id, p.name
ORDER BY logged_time DESC;`;
@@ -372,7 +381,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
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 ) {
if (project.value > 0) {
data.push(project);
}
@@ -392,6 +401,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
const projects = (req.body.projects || []) as string[];
const projectIds = projects.map(p => `'${p}'`).join(",");
const billable = req.body.billable;
if (!teamIds || !projectIds.length)
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
@@ -402,12 +413,14 @@ export default class ReportingAllocationController extends ReportingControllerBa
? ""
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
const billableQuery = this.buildBillableQuery(billable);
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
LEFT JOIN tasks ON tasks.id = task_work_log.task_id ${billableQuery}
LEFT JOIN projects p ON p.id = tasks.project_id AND p.team_id = tmiv.team_id
WHERE p.id IN (${projectIds})
${durationClause} ${archivedClause}
GROUP BY tmiv.email, tmiv.name
@@ -422,7 +435,64 @@ export default class ReportingAllocationController extends ReportingControllerBa
return res.status(200).send(new ServerResponse(true, result.rows));
}
@HandleExceptions()
public static async exportTest(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const archived = req.query.archived === "true";
const teamId = this.getCurrentTeamId(req);
const { duration, date_range } = req.query;
const durationClause = this.getDateRangeClause(duration as string || DATE_RANGES.LAST_WEEK, date_range 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 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 in_organization(p.team_id, $1)
${durationClause} ${archivedClause}
GROUP BY p.id, p.name
ORDER BY p.name ASC;`;
const result = await db.query(q, [teamId]);
const labelsX = [];
const dataX = [];
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;
labelsX.push(project.name);
dataX.push(project.value || 0);
}
const chart = new ChartJsImage();
chart.setConfig({
type: "bar",
data: {
labels: labelsX,
datasets: [
{ label: "", data: dataX }
]
},
});
chart.setWidth(1920).setHeight(1080).setBackgroundColor("transparent");
const url = chart.getUrl();
chart.toFile("test.png");
return res.status(200).send(new ServerResponse(true, url));
}
private static getEstimated(project: any, type: string) {
// if (project.estimated_man_days === 0 || project.estimated_working_days === 0) {
// return (parseFloat(moment.duration(project.estimated, "minutes").asHours().toFixed(2)) / int(project.hours_per_day)).toFixed(2)
// }
switch (type) {
case IToggleOptions.MAN_DAYS:
@@ -445,7 +515,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
const projects = (req.body.projects || []) as string[];
const projectIds = projects.map(p => `'${p}'`).join(",");
const { type } = req.body;
const { type, billable } = req.body;
if (!teamIds || !projectIds.length)
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
@@ -458,6 +528,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
? ""
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
const billableQuery = this.buildBillableQuery(billable);
const q = `
SELECT p.id,
p.name,
@@ -471,8 +543,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
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
LEFT JOIN tasks ON tasks.project_id = p.id ${billableQuery}
LEFT JOIN task_work_log ON task_work_log.task_id = tasks.id
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
GROUP BY p.id, p.name
ORDER BY logged_time DESC;`;
@@ -491,7 +563,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
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 ) {
if (project.value > 0 || project.estimated_value > 0) {
data.push(project);
}

View File

@@ -109,6 +109,23 @@ export default abstract class ReportingControllerBase extends WorklenzController
return "";
}
protected static buildBillableQuery(selectedStatuses: { billable: boolean; nonBillable: boolean }): string {
const { billable, nonBillable } = selectedStatuses;
if (billable && nonBillable) {
// Both are enabled, no need to filter
return "";
} else if (billable) {
// Only billable is enabled
return " AND tasks.billable IS TRUE";
} else if (nonBillable) {
// Only non-billable is enabled
return " AND tasks.billable IS FALSE";
}
return "";
}
protected static formatEndDate(endDate: string) {
const end = moment(endDate).format("YYYY-MM-DD");
const fEndDate = moment(end);
@@ -173,6 +190,9 @@ export default abstract class ReportingControllerBase extends WorklenzController
(SELECT color_code
FROM sys_project_healths
WHERE sys_project_healths.id = p.health_id) AS health_color,
(SELECT name
FROM sys_project_healths
WHERE sys_project_healths.id = p.health_id) AS health_name,
pc.id AS category_id,
pc.name AS category_name,

View File

@@ -862,7 +862,7 @@ export default class ReportingMembersController extends ReportingControllerBase
}
private static async memberTimeLogsData(durationClause: string, minMaxDateClause: string, team_id: string, team_member_id: string, includeArchived: boolean, userId: string) {
private static async memberTimeLogsData(durationClause: string, minMaxDateClause: string, team_id: string, team_member_id: string, includeArchived: boolean, userId: string, billableQuery = "") {
const archivedClause = includeArchived
? ""
@@ -884,7 +884,7 @@ export default class ReportingMembersController extends ReportingControllerBase
FROM task_work_log twl
WHERE twl.user_id = tmiv.user_id
${durationClause}
AND task_id IN (SELECT id FROM tasks WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1) ${archivedClause} )
AND task_id IN (SELECT id FROM tasks WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1) ${archivedClause} ${billableQuery})
ORDER BY twl.updated_at DESC) tl) AS time_logs
${minMaxDateClause}
FROM team_member_info_view tmiv
@@ -1017,14 +1017,33 @@ export default class ReportingMembersController extends ReportingControllerBase
}
protected static buildBillableQuery(selectedStatuses: { billable: boolean; nonBillable: boolean }): string {
const { billable, nonBillable } = selectedStatuses;
if (billable && nonBillable) {
// Both are enabled, no need to filter
return "";
} else if (billable) {
// Only billable is enabled
return " AND tasks.billable IS TRUE";
} else if (nonBillable) {
// Only non-billable is enabled
return " AND tasks.billable IS FALSE";
}
return "";
}
@HandleExceptions()
public static async getMemberTimelogs(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { team_member_id, team_id, duration, date_range, archived } = req.body;
const { team_member_id, team_id, duration, date_range, archived, billable } = req.body;
const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration || DATE_RANGES.LAST_WEEK, date_range, "twl");
const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range, "task_work_log");
const logGroups = await this.memberTimeLogsData(durationClause, minMaxDateClause, team_id, team_member_id, archived, req.user?.id as string);
const billableQuery = this.buildBillableQuery(billable);
const logGroups = await this.memberTimeLogsData(durationClause, minMaxDateClause, team_id, team_member_id, archived, req.user?.id as string, billableQuery);
return res.status(200).send(new ServerResponse(true, logGroups));
}
@@ -1049,6 +1068,7 @@ export default class ReportingMembersController extends ReportingControllerBase
const completedDurationClasue = this.completedDurationFilter(duration as string, dateRange);
const overdueClauseByDate = this.getActivityLogsOverdue(duration as string, dateRange);
const taskSelectorClause = this.getTaskSelectorClause();
const durationFilter = this.memberTasksDurationFilter(duration as string, dateRange);
const q = `
SELECT name AS team_member_name,
@@ -1059,6 +1079,12 @@ export default class ReportingMembersController extends ReportingControllerBase
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 ${assignClause} ${archivedClause}) assigned) AS assigned,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(assigned))), '[]')
FROM (${taskSelectorClause}
FROM tasks t
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE ta.team_member_id = $1 ${durationFilter} ${assignClause} ${archivedClause}) assigned) AS total,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(completed))), '[]')
FROM (${taskSelectorClause}
FROM tasks t
@@ -1095,6 +1121,11 @@ export default class ReportingMembersController extends ReportingControllerBase
const body = {
team_member_name: data.team_member_name,
groups: [
{
name: "Total Tasks",
color_code: "#7590c9",
tasks: data.total ? data.total : 0
},
{
name: "Tasks Assigned",
color_code: "#7590c9",
@@ -1114,7 +1145,7 @@ export default class ReportingMembersController extends ReportingControllerBase
name: "Tasks Ongoing",
color_code: "#7cb5ec",
tasks: data.ongoing ? data.ongoing : 0
}
},
]
};