init
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user