Files
worklenz/worklenz-backend/src/controllers/schedule/schedule-controller.ts
chamikaJ 8825b0410a init
2025-04-17 18:28:54 +05:30

946 lines
37 KiB
TypeScript

import db from "../../config/db";
import { ParsedQs } from "qs";
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 { TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA, UNMAPPED } from "../../shared/constants";
import { getColor } from "../../shared/utils";
import moment, { Moment } from "moment";
import momentTime from "moment-timezone";
import ScheduleTasksControllerBase, { GroupBy, IScheduleTaskGroup } from "./schedule-controller-base";
interface IDateUnions {
date_union: {
start_date: string | null;
end_date: string | null;
},
logs_date_union: {
start_date: string | null;
end_date: string | null;
},
allocated_date_union: {
start_date: string | null;
end_date: string | null;
}
}
interface IDatesPair {
start_date: string | null,
end_date: string | null
}
export class IScheduleTaskListGroup implements IScheduleTaskGroup {
name: string;
category_id: string | null;
color_code: string;
tasks: any[];
isExpand: boolean;
constructor(group: any) {
this.name = group.name;
this.category_id = group.category_id || null;
this.color_code = group.color_code + TASK_STATUS_COLOR_ALPHA;
this.tasks = [];
this.isExpand = group.isExpand;
}
}
export default class ScheduleControllerV2 extends ScheduleTasksControllerBase {
private static GLOBAL_DATE_WIDTH = 35;
private static GLOBAL_START_DATE = moment().format("YYYY-MM-DD");
private static GLOBAL_END_DATE = moment().format("YYYY-MM-DD");
// Migrate data
@HandleExceptions()
public static async migrate(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const getDataq = `SELECT p.id,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
FROM (SELECT tmiv.team_member_id,
tmiv.user_id,
LEAST(
(SELECT MIN(LEAST(start_date, end_date)) AS start_date
FROM tasks
INNER JOIN tasks_assignees ta ON tasks.id = ta.task_id
WHERE archived IS FALSE
AND project_id = p.id
AND ta.team_member_id = tmiv.team_member_id),
(SELECT MIN(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS ll_start_date
FROM task_work_log twl
INNER JOIN tasks t ON twl.task_id = t.id AND t.archived IS FALSE
WHERE t.project_id = p.id
AND twl.user_id = tmiv.user_id)
) AS lowest_date,
GREATEST(
(SELECT MAX(GREATEST(start_date, end_date)) AS end_date
FROM tasks
INNER JOIN tasks_assignees ta ON tasks.id = ta.task_id
WHERE archived IS FALSE
AND project_id = p.id
AND ta.team_member_id = tmiv.team_member_id),
(SELECT MAX(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS ll_end_date
FROM task_work_log twl
INNER JOIN tasks t ON twl.task_id = t.id AND t.archived IS FALSE
WHERE t.project_id = p.id
AND twl.user_id = tmiv.user_id)
) AS greatest_date
FROM project_members pm
INNER JOIN team_member_info_view tmiv
ON pm.team_member_id = tmiv.team_member_id
WHERE project_id = p.id) rec) AS members
FROM projects p
WHERE team_id IS NOT NULL
AND p.id NOT IN (SELECT project_id FROM archived_projects)`;
const projectMembersResults = await db.query(getDataq);
const projectMemberData = projectMembersResults.rows;
const arrayToInsert = [];
for (const data of projectMemberData) {
if (data.members.length) {
for (const member of data.members) {
const body = {
project_id: data.id,
team_member_id: member.team_member_id,
allocated_from: member.lowest_date ? member.lowest_date : null,
allocated_to: member.greatest_date ? member.greatest_date : null
};
if (body.allocated_from && body.allocated_to) arrayToInsert.push(body);
}
}
}
const insertArray = JSON.stringify(arrayToInsert);
const insertFunctionCall = `SELECT migrate_member_allocations($1)`;
await db.query(insertFunctionCall, [insertArray]);
return res.status(200).send(new ServerResponse(true, ""));
}
private static async getFirstLastDates(teamId: string, userId: string) {
const q = `SELECT MIN(LEAST(allocated_from, allocated_to)) AS start_date,
MAX(GREATEST(allocated_from, allocated_to)) AS end_date,
(SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
FROM (SELECT MIN(min_date) AS start_date, MAX(max_date) AS end_date
FROM (SELECT MIN(start_date) AS min_date, MAX(start_date) AS max_date
FROM tasks
WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1)
AND project_id NOT IN
(SELECT project_id
FROM archived_projects
WHERE user_id = $2)
AND tasks.archived IS FALSE
UNION
SELECT MIN(end_date) AS min_date, MAX(end_date) AS max_date
FROM tasks
WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1)
AND project_id NOT IN
(SELECT project_id
FROM archived_projects
WHERE user_id = $2)
AND tasks.archived IS FALSE) AS dates) rec) AS date_union,
(SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
FROM (SELECT MIN(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS start_date,
MAX(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS end_date
FROM task_work_log twl
INNER JOIN tasks t ON twl.task_id = t.id AND t.archived IS FALSE
WHERE t.project_id IN (SELECT id FROM projects WHERE team_id = $1)
AND project_id NOT IN
(SELECT project_id
FROM archived_projects
WHERE user_id = $2)) rec) AS logs_date_union
FROM project_member_allocations
WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1)`;
const res = await db.query(q, [teamId, userId]);
return res.rows[0];
}
private static validateEndDate(endDate: Moment): boolean {
return endDate.isBefore(moment(), "day");
}
private static validateStartDate(startDate: Moment): boolean {
return startDate.isBefore(moment(), "day");
}
private static getScrollAmount(startDate: Moment) {
const today = moment();
const daysDifference = today.diff(startDate, "days");
return (this.GLOBAL_DATE_WIDTH * daysDifference);
}
private static setAllocationIndicator(item: any) {
if (moment(item.allocated_from).isValid() && moment(item.allocated_to).isValid()) {
const daysFromStart = moment(item.allocated_from).diff(this.GLOBAL_START_DATE, "days");
const indicatorOffset = daysFromStart * this.GLOBAL_DATE_WIDTH;
const daysDifference = moment(item.allocated_to).diff(item.allocated_from, "days");
const indicatorWidth = (daysDifference + 1) * this.GLOBAL_DATE_WIDTH;
return { indicatorOffset, indicatorWidth };
}
return null;
}
private static setIndicatorWithLogIndicator(item: any) {
const daysFromStart = moment(item.start_date).diff(this.GLOBAL_START_DATE, "days");
const indicatorOffset = daysFromStart * this.GLOBAL_DATE_WIDTH;
const daysDifference = moment(item.end_date).diff(item.start_date, "days");
const indicatorWidth = (daysDifference + 1) * this.GLOBAL_DATE_WIDTH;
let logIndicatorOffset = 0;
let logIndicatorWidth = 0;
if (item.logs_date_union && item.logs_date_union.start_date && item.logs_date_union.end_date) {
const daysFromIndicatorStart = moment(item.logs_date_union.start_date).diff(item.start_date, "days");
logIndicatorOffset = daysFromIndicatorStart * this.GLOBAL_DATE_WIDTH;
const daysDifferenceFromIndicator = moment(item.logs_date_union.end_date).diff(item.logs_date_union.start_date, "days");
logIndicatorWidth = (daysDifferenceFromIndicator + 1) * this.GLOBAL_DATE_WIDTH;
}
const body = {
indicatorOffset,
indicatorWidth,
logIndicatorOffset,
logIndicatorWidth
};
return body;
}
private static async setChartStartEnd(dateRange: IDatesPair, logsRange: IDatesPair, allocatedRange: IDatesPair, timeZone: string) {
const datesToCheck = [];
const body = {
date_union: {
start_date: dateRange.start_date ? momentTime.tz(dateRange.start_date, `${timeZone}`).format("YYYY-MM-DD") : null,
end_date: dateRange.end_date ? momentTime.tz(dateRange.end_date, `${timeZone}`).format("YYYY-MM-DD") : null,
},
logs_date_union: {
start_date: logsRange.start_date ? momentTime.tz(logsRange.start_date, `${timeZone}`).format("YYYY-MM-DD") : null,
end_date: logsRange.end_date ? momentTime.tz(logsRange.end_date, `${timeZone}`).format("YYYY-MM-DD") : null,
},
allocated_date_union: {
start_date: allocatedRange.start_date ? momentTime.tz(allocatedRange.start_date, `${timeZone}`).format("YYYY-MM-DD") : null,
end_date: allocatedRange.end_date ? momentTime.tz(allocatedRange.end_date, `${timeZone}`).format("YYYY-MM-DD") : null,
}
};
for (const dateKey in body) {
if (body[dateKey as keyof IDateUnions] && body[dateKey as keyof IDateUnions].start_date) {
datesToCheck.push(moment(body[dateKey as keyof IDateUnions].start_date));
}
if (body[dateKey as keyof IDateUnions] && body[dateKey as keyof IDateUnions].end_date) {
datesToCheck.push(moment(body[dateKey as keyof IDateUnions].end_date));
}
}
const validDateToCheck = datesToCheck.filter((date) => date.isValid());
dateRange.start_date = moment.min(validDateToCheck).format("YYYY-MM-DD");
dateRange.end_date = moment.max(validDateToCheck).format("YYYY-MM-DD");
return dateRange;
}
private static async mainDateValidator(dateRange: any) {
const today = new Date();
let startDate = moment(today).clone().startOf("year");
let endDate = moment(today).clone().endOf("year").add(1, "year");
if (dateRange.start_date && dateRange.end_date) {
startDate = this.validateStartDate(moment(dateRange.start_date)) ? moment(dateRange.start_date).startOf("year") : moment(today).clone().startOf("year");
endDate = this.validateEndDate(moment(dateRange.end_date)) ? moment(today).clone().endOf("year") : moment(dateRange.end_date).endOf("year");
} else if (dateRange.start_date && !dateRange.end_date) {
startDate = this.validateStartDate(moment(dateRange.start_date)) ? moment(dateRange.start_date).startOf("year") : moment(today).clone().startOf("year");
} else if (!dateRange.start_date && dateRange.end_date) {
endDate = this.validateEndDate(moment(dateRange.end_date)) ? moment(today).clone().endOf("year") : moment(dateRange.end_date).endOf("year");
}
return { startDate, endDate, today };
}
private static async createDateColumns(xMonthsBeforeStart: Moment, xMonthsAfterEnd: Moment, today: Date) {
const dateData = [];
let days = -1;
const currentDate = xMonthsBeforeStart.clone();
while (currentDate.isBefore(xMonthsAfterEnd)) {
const monthData = {
month: currentDate.format("MMM YYYY"),
weeks: [] as number[],
days: [] as { day: number, name: string, isWeekend: boolean, isToday: boolean }[],
};
const daysInMonth = currentDate.daysInMonth();
for (let day = 1; day <= daysInMonth; day++) {
const dayOfMonth = currentDate.date();
const dayName = currentDate.format("ddd");
const isWeekend = [0, 6].includes(currentDate.day());
const isToday = moment(moment(today).format("YYYY-MM-DD")).isSame(moment(currentDate).format("YYYY-MM-DD"));
monthData.days.push({ day: dayOfMonth, name: dayName, isWeekend, isToday });
currentDate.add(1, "day");
days++;
}
dateData.push(monthData);
}
return { dateData, days };
}
@HandleExceptions()
public static async createDateRange(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const dates = await this.getFirstLastDates(req.params.id as string, req.user?.id as string);
const dateRange = dates.date_union;
const logsRange = dates.logs_date_union;
const allocatedRange = { start_date: dates.start_date, end_date: dates.end_date };
await this.setChartStartEnd(dateRange, logsRange, allocatedRange, req.query.timeZone as string);
const { startDate, endDate, today } = await this.mainDateValidator(dateRange);
const xMonthsBeforeStart = startDate.clone().subtract(3, "months");
const xMonthsAfterEnd = endDate.clone().add(2, "year");
this.GLOBAL_START_DATE = moment(xMonthsBeforeStart).format("YYYY-MM-DD");
this.GLOBAL_END_DATE = moment(xMonthsAfterEnd).format("YYYY-MM-DD");
const { dateData, days } = await this.createDateColumns(xMonthsBeforeStart, xMonthsAfterEnd, today);
const scrollBy = await this.getScrollAmount(xMonthsBeforeStart);
const result = {
date_data: dateData,
width: days + 1,
scroll_by: scrollBy,
chart_start: moment(this.GLOBAL_START_DATE).format("YYYY-MM-DD"),
chart_end: moment(this.GLOBAL_END_DATE).format("YYYY-MM-DD")
};
return res.status(200).send(new ServerResponse(true, result));
}
private static async getProjectsQuery(teamId: string, userId: string) {
const q = `SELECT p.id,
p.name,
(SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
FROM (SELECT MIN(min_date) AS start_date, MAX(max_date) AS end_date
FROM (SELECT MIN(allocated_from) AS min_date, MAX(allocated_from) AS max_date
FROM project_member_allocations
WHERE project_id = p.id
UNION
SELECT MIN(allocated_to) AS min_date, MAX(allocated_to) AS max_date
FROM project_member_allocations
WHERE project_id = p.id) AS dates) rec) AS date_union,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
FROM (SELECT pm.id AS project_member_id,
tmiv.team_member_id,
tmiv.user_id,
name AS name,
avatar_url,
TRUE AS project_member,
EXISTS(SELECT email
FROM email_invitations
WHERE team_member_id = tmiv.team_member_id
AND email_invitations.team_id = $1) AS pending_invitation,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
FROM (SELECT
pma.id,
pma.allocated_from,
pma.allocated_to
FROM project_member_allocations pma
WHERE pma.team_member_id = tmiv.team_member_id
AND pma.project_id = p.id) rec)
AS allocations
FROM project_members pm
INNER JOIN team_member_info_view tmiv
ON pm.team_member_id = tmiv.team_member_id
WHERE project_id = p.id
ORDER BY NAME ASC) rec) AS members
FROM projects p
WHERE team_id = $1
AND p.id NOT IN
(SELECT project_id FROM archived_projects WHERE user_id = $2)
ORDER BY p.name`;
const result = await db.query(q, [teamId, userId]);
return result;
}
@HandleExceptions()
public static async getProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const userId = req.user?.id as string;
const teamId = req.params.id as string;
const timeZone = req.query.timeZone as string;
const result = await this.getProjectsQuery(teamId, userId);
for (const project of result.rows) {
const { lowestDate, highestDate } = await this.setIndicatorDates(project, timeZone);
project.allocated_from = lowestDate ? moment(lowestDate).format("YYYY-MM-DD") : null;
project.allocated_to = highestDate ? moment(highestDate).format("YYYY-MM-DD") : null;
const styles = this.setAllocationIndicator(project);
project.indicator_offset = styles?.indicatorOffset && project.members.length ? styles.indicatorOffset : 0;
project.indicator_width = styles?.indicatorWidth && project.members.length ? styles.indicatorWidth : 0;
project.color_code = getColor(project.name);
for (const member of project.members) {
const mergedAllocation = await this.mergeAllocations(member.allocations);
member.allocations = mergedAllocation;
for (const allocation of member.allocations) {
allocation.allocated_from = allocation.allocated_from ? momentTime.tz(allocation.allocated_from, `${timeZone}`).format("YYYY-MM-DD") : null;
allocation.allocated_to = allocation.allocated_to ? momentTime.tz(allocation.allocated_to, `${timeZone}`).format("YYYY-MM-DD") : null;
const styles = this.setAllocationIndicator(allocation);
allocation.indicator_offset = styles?.indicatorOffset ? styles?.indicatorOffset : 0;
allocation.indicator_width = styles?.indicatorWidth ? styles?.indicatorWidth : 0;
}
member.color_code = getColor(member.name);
}
}
return res.status(200).send(new ServerResponse(true, result.rows));
}
@HandleExceptions()
public static async getSingleProjectIndicator(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const projectId = req.params.id as string;
const teamMemberId = req.query.team_member_id as string;
const timeZone = req.query.timeZone as string;
const projectIndicatorRefresh = req.query.isProjectRefresh;
const q = `SELECT id,
allocated_from,
allocated_to,
(SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
FROM (SELECT MIN(min_date) AS start_date, MAX(max_date) AS end_date
FROM (SELECT MIN(allocated_from) AS min_date, MAX(allocated_from) AS max_date
FROM project_member_allocations
WHERE project_id = $1
UNION
SELECT MIN(allocated_to) AS min_date, MAX(allocated_to) AS max_date
FROM project_member_allocations
WHERE project_id = $1) AS dates) rec) AS date_union
FROM project_member_allocations
WHERE team_member_id = $2
AND project_id = $1`;
const result = await db.query(q, [projectId, teamMemberId]);
const body = {
project_allocation: { start_date: null, end_date: null, indicator_offset: null, indicator_width: null },
member_allocations: [{}]
};
if (result.rows.length) {
const mergedAllocation = await this.mergeAllocations(result.rows);
result.rows = mergedAllocation;
for (const allocation of result.rows) {
allocation.allocated_from = allocation.allocated_from ? momentTime.tz(allocation.allocated_from, `${timeZone}`).format("YYYY-MM-DD") : null;
allocation.allocated_to = allocation.allocated_to ? momentTime.tz(allocation.allocated_to, `${timeZone}`).format("YYYY-MM-DD") : null;
const styles = this.setAllocationIndicator(allocation);
allocation.indicator_offset = styles?.indicatorOffset ? styles?.indicatorOffset : 0;
allocation.indicator_width = styles?.indicatorWidth ? styles?.indicatorWidth : 0;
}
body.member_allocations = result.rows;
}
const qP = `SELECT id,
allocated_from,
allocated_to,
(SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
FROM (SELECT MIN(min_date) AS start_date, MAX(max_date) AS end_date
FROM (SELECT MIN(allocated_from) AS min_date, MAX(allocated_from) AS max_date
FROM project_member_allocations
WHERE project_id = $1
UNION
SELECT MIN(allocated_to) AS min_date, MAX(allocated_to) AS max_date
FROM project_member_allocations
WHERE project_id = $1) AS dates) rec) AS date_union
FROM project_member_allocations
WHERE project_id = $1`;
const resultP = await db.query(qP, [projectId]);
if (resultP.rows.length) {
const project = resultP.rows[0];
const { lowestDate, highestDate } = await this.setIndicatorDates(project, timeZone);
if (lowestDate) project.start_date = lowestDate;
if (highestDate) project.end_date = highestDate;
project.start_date = project.start_date ? moment(project.start_date).format("YYYY-MM-DD") : moment().format("YYYY-MM-DD");
project.end_date = project.end_date ? moment(project.end_date).format("YYYY-MM-DD") : moment().format("YYYY-MM-DD");
const styles = this.setIndicatorWithLogIndicator(project);
project.indicator_offset = styles.indicatorOffset;
project.indicator_width = styles.indicatorWidth;
body.project_allocation = project;
}
return res.status(200).send(new ServerResponse(true, body));
}
@HandleExceptions()
public static async getSingleProjectMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const projectId = req.params.id as string;
const teamMemberId = req.query.team_member_id as string;
const timeZone = req.query.timeZone as string;
const projectIndicatorRefresh = req.query.isProjectRefresh;
const q = `SELECT id,
allocated_from,
allocated_to,
(SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
FROM (SELECT MIN(min_date) AS start_date, MAX(max_date) AS end_date
FROM (SELECT MIN(allocated_from) AS min_date, MAX(allocated_from) AS max_date
FROM project_member_allocations
WHERE project_id = $1
UNION
SELECT MIN(allocated_to) AS min_date, MAX(allocated_to) AS max_date
FROM project_member_allocations
WHERE project_id = $1) AS dates) rec) AS date_union
FROM project_member_allocations
WHERE team_member_id = $2
AND project_id = $1`;
const result = await db.query(q, [projectId, teamMemberId]);
const body = {
project_allocation: { start_date: null, end_date: null, indicator_offset: null, indicator_width: null },
member_allocations: [{}]
};
if (result.rows.length) {
const project = result.rows[0];
const { lowestDate, highestDate } = await this.setIndicatorDates(project, timeZone);
if (lowestDate) project.start_date = lowestDate;
if (highestDate) project.end_date = highestDate;
project.start_date = project.start_date ? moment(project.start_date).format("YYYY-MM-DD") : moment().format("YYYY-MM-DD");
project.end_date = project.end_date ? moment(project.end_date).format("YYYY-MM-DD") : moment().format("YYYY-MM-DD");
const styles = this.setIndicatorWithLogIndicator(project);
project.indicator_offset = styles.indicatorOffset;
project.indicator_width = styles.indicatorWidth;
const mergedAllocation = await this.mergeAllocations(result.rows);
result.rows = mergedAllocation;
for (const allocation of result.rows) {
allocation.allocated_from = allocation.allocated_from ? momentTime.tz(allocation.allocated_from, `${timeZone}`).format("YYYY-MM-DD") : null;
allocation.allocated_to = allocation.allocated_to ? momentTime.tz(allocation.allocated_to, `${timeZone}`).format("YYYY-MM-DD") : null;
const styles = this.setAllocationIndicator(allocation);
allocation.indicator_offset = styles?.indicatorOffset ? styles?.indicatorOffset : 0;
allocation.indicator_width = styles?.indicatorWidth ? styles?.indicatorWidth : 0;
}
body.member_allocations = result.rows;
body.project_allocation = project;
}
return res.status(200).send(new ServerResponse(true, body));
}
private static async mergeAllocations(allocations: { id: string | null, allocated_from: string | null, allocated_to: string | null, indicator_offset: number, indicator_width: number }[]) {
if (!allocations.length) return [];
allocations.sort((a, b) => moment(a.allocated_from).diff(moment(b.allocated_from)));
const mergedRanges = [];
let currentRange = { ...allocations[0], ids: [allocations[0].id] };
for (let i = 1; i < allocations.length; i++) {
const nextRange = allocations[i];
if (moment(currentRange.allocated_to).isSameOrAfter(nextRange.allocated_from)) {
currentRange.allocated_to = moment.max(moment(currentRange.allocated_to), moment(nextRange.allocated_to)).toISOString();
currentRange.ids.push(nextRange.id);
} else {
mergedRanges.push({ ...currentRange });
currentRange = { ...nextRange, ids: [nextRange.id] };
}
}
mergedRanges.push({ ...currentRange });
return mergedRanges;
}
private static async setIndicatorDates(item: any, timeZone: string) {
const datesToCheck = [];
item.date_union.start_date = item.date_union.start_date ? momentTime.tz(item.date_union.start_date, `${timeZone}`).format("YYYY-MM-DD") : null;
item.date_union.end_date = item.date_union.end_date ? momentTime.tz(item.date_union.end_date, `${timeZone}`).format("YYYY-MM-DD") : null;
for (const dateKey in item) {
if (item[dateKey as keyof IDateUnions] && item[dateKey as keyof IDateUnions].start_date) {
datesToCheck.push(moment(item[dateKey as keyof IDateUnions].start_date));
}
if (item[dateKey as keyof IDateUnions] && item[dateKey as keyof IDateUnions].end_date) {
datesToCheck.push(moment(item[dateKey as keyof IDateUnions].end_date));
}
}
const validDateToCheck = datesToCheck.filter((date) => date.isValid());
const lowestDate = validDateToCheck.length ? moment.min(validDateToCheck).format("YYYY-MM-DD") : null;
const highestDate = validDateToCheck.length ? moment.max(validDateToCheck).format("YYYY-MM-DD") : null;
return {
lowestDate,
highestDate
};
}
@HandleExceptions()
public static async deleteMemberAllocations(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const ids = req.body.toString() as string;
const q = `DELETE FROM project_member_allocations WHERE id IN (${(ids || "").split(",").map(s => `'${s}'`).join(",")})`;
await db.query(q);
return res.status(200).send(new ServerResponse(true, []));
}
// ********************************************
private static isCountsOnly(query: ParsedQs) {
return query.count === "true";
}
public static isTasksOnlyReq(query: ParsedQs) {
return ScheduleControllerV2.isCountsOnly(query) || query.parent_task;
}
private static flatString(text: string) {
return (text || "").split(" ").map(s => `'${s}'`).join(",");
}
private static getFilterByMembersWhereClosure(text: string) {
return text
? `id IN (SELECT task_id FROM tasks_assignees WHERE team_member_id IN (${this.flatString(text)}))`
: "";
}
private static getStatusesQuery(filterBy: string) {
return filterBy === "member"
? `, (SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
FROM (SELECT task_statuses.id, task_statuses.name, stsc.color_code
FROM task_statuses
INNER JOIN sys_task_status_categories stsc ON task_statuses.category_id = stsc.id
WHERE project_id = t.project_id
ORDER BY task_statuses.name) rec) AS statuses`
: "";
}
public static async getTaskCompleteRatio(taskId: string): Promise<{
ratio: number;
total_completed: number;
total_tasks: number;
} | null> {
try {
const result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [taskId]);
const [data] = result.rows;
data.info.ratio = +data.info.ratio.toFixed();
return data.info;
} catch (error) {
return null;
}
}
private static getQuery(userId: string, options: ParsedQs) {
const searchField = options.search ? "t.name" : "sort_order";
const { searchQuery, sortField } = ScheduleControllerV2.toPaginationOptions(options, searchField);
const isSubTasks = !!options.parent_task;
const sortFields = sortField.replace(/ascend/g, "ASC").replace(/descend/g, "DESC") || "sort_order";
const membersFilter = ScheduleControllerV2.getFilterByMembersWhereClosure(options.members as string);
const statusesQuery = ScheduleControllerV2.getStatusesQuery(options.filterBy as string);
const archivedFilter = options.archived === "true" ? "archived IS TRUE" : "archived IS FALSE";
let subTasksFilter;
if (options.isSubtasksInclude === "true") {
subTasksFilter = "";
} else {
subTasksFilter = isSubTasks ? "parent_task_id = $2" : "parent_task_id IS NULL";
}
const filters = [
subTasksFilter,
(isSubTasks ? "1 = 1" : archivedFilter),
membersFilter
].filter(i => !!i).join(" AND ");
return `
SELECT id,
name,
t.project_id AS project_id,
t.parent_task_id,
t.parent_task_id IS NOT NULL AS is_sub_task,
(SELECT name FROM tasks WHERE id = t.parent_task_id) AS parent_task_name,
(SELECT COUNT(*)
FROM tasks
WHERE parent_task_id = t.id)::INT AS sub_tasks_count,
t.status_id AS status,
t.archived,
t.sort_order,
(SELECT phase_id FROM task_phase WHERE task_id = t.id) AS phase_id,
(SELECT name
FROM project_phases
WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_name,
(SELECT color_code
FROM sys_task_status_categories
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
(SELECT COALESCE(ROW_TO_JSON(r), '{}'::JSON)
FROM (SELECT is_done, is_doing, is_todo
FROM sys_task_status_categories
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) r) AS status_category,
(CASE
WHEN EXISTS(SELECT 1
FROM tasks_with_status_view
WHERE tasks_with_status_view.task_id = t.id
AND is_done IS TRUE) THEN 1
ELSE 0 END) AS parent_task_completed,
(SELECT get_task_assignees(t.id)) AS assignees,
(SELECT COUNT(*)
FROM tasks_with_status_view tt
WHERE tt.parent_task_id = t.id
AND tt.is_done IS TRUE)::INT
AS completed_sub_tasks,
(SELECT id FROM task_priorities WHERE id = t.priority_id) AS priority,
(SELECT value FROM task_priorities WHERE id = t.priority_id) AS priority_value,
total_minutes,
start_date,
end_date ${statusesQuery}
FROM tasks t
WHERE ${filters} ${searchQuery} AND project_id = $1
ORDER BY end_date DESC NULLS LAST
`;
}
public static async getGroups(groupBy: string, projectId: string): Promise<IScheduleTaskGroup[]> {
let q = "";
let params: any[] = [];
switch (groupBy) {
case GroupBy.STATUS:
q = `
SELECT id,
name,
(SELECT color_code FROM sys_task_status_categories WHERE id = task_statuses.category_id),
category_id
FROM task_statuses
WHERE project_id = $1
ORDER BY sort_order;
`;
params = [projectId];
break;
case GroupBy.PRIORITY:
q = `SELECT id, name, color_code
FROM task_priorities
ORDER BY value DESC;`;
break;
case GroupBy.LABELS:
q = `
SELECT id, name, color_code
FROM team_labels
WHERE team_id = $2
AND EXISTS(SELECT 1
FROM tasks
WHERE project_id = $1
AND EXISTS(SELECT 1 FROM task_labels WHERE task_id = tasks.id AND label_id = team_labels.id))
ORDER BY name;
`;
break;
case GroupBy.PHASE:
q = `
SELECT id, name, color_code, start_date, end_date
FROM project_phases
WHERE project_id = $1
ORDER BY name;
`;
params = [projectId];
break;
default:
break;
}
const result = await db.query(q, params);
for (const row of result.rows) {
row.isExpand = true;
}
return result.rows;
}
@HandleExceptions()
public static async getList(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const isSubTasks = !!req.query.parent_task;
const groupBy = (req.query.group || GroupBy.STATUS) as string;
const q = ScheduleControllerV2.getQuery(req.user?.id as string, req.query);
const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
const result = await db.query(q, params);
const tasks = [...result.rows];
const groups = await this.getGroups(groupBy, req.params.id);
const map = groups.reduce((g: { [x: string]: IScheduleTaskGroup }, group) => {
if (group.id)
g[group.id] = new IScheduleTaskListGroup(group);
return g;
}, {});
this.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));
}
public static updateMapByGroup(tasks: any[], groupBy: string, map: { [p: string]: IScheduleTaskGroup }) {
let index = 0;
const unmapped = [];
for (const task of tasks) {
task.index = index++;
ScheduleControllerV2.updateTaskViewModel(task);
if (groupBy === GroupBy.STATUS) {
map[task.status]?.tasks.push(task);
} else if (groupBy === GroupBy.PRIORITY) {
map[task.priority]?.tasks.push(task);
} else if (groupBy === GroupBy.PHASE && task.phase_id) {
map[task.phase_id]?.tasks.push(task);
} else {
unmapped.push(task);
}
}
if (unmapped.length) {
map[UNMAPPED] = {
name: UNMAPPED,
category_id: null,
color_code: "#f0f0f0",
tasks: unmapped,
isExpand: true
};
}
}
@HandleExceptions()
public static async getTasksOnly(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const isSubTasks = !!req.query.parent_task;
const q = ScheduleControllerV2.getQuery(req.user?.id as string, req.query);
const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
const result = await db.query(q, params);
let data: any[] = [];
// if true, we only return the record count
if (this.isCountsOnly(req.query)) {
[data] = result.rows;
} else { // else we return a flat list of tasks
data = [...result.rows];
for (const task of data) {
ScheduleControllerV2.updateTaskViewModel(task);
}
}
return res.status(200).send(new ServerResponse(true, data));
}
}