Initial commit: Angular frontend and Expressjs backend

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

View File

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

View File

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

View File

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