Initial commit: Angular frontend and Expressjs backend
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
export interface IProjectTemplateLabel {
|
||||
label_id?: string;
|
||||
name?: string;
|
||||
color_code?: string;
|
||||
}
|
||||
|
||||
export interface IProjectTemplate {
|
||||
name?: string;
|
||||
id?: string;
|
||||
key?: string;
|
||||
description?: string;
|
||||
phase_label?: string;
|
||||
phases?: any;
|
||||
tasks?: any;
|
||||
status?: any;
|
||||
}
|
||||
|
||||
export interface IProjectTemplatePhase {
|
||||
id?: string;
|
||||
name?: string;
|
||||
color_code?: string;
|
||||
}
|
||||
|
||||
export interface IProjectTemplateStatus {
|
||||
id?: string;
|
||||
name?: string;
|
||||
category_id?: string;
|
||||
category_name?: string;
|
||||
sort_order?: string;
|
||||
}
|
||||
|
||||
export interface IProjectTaskPhase {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface IProjectTemplateTask {
|
||||
id?: string;
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
total_minutes?: number;
|
||||
sort_order?: number;
|
||||
priority_id?: string;
|
||||
priority_name?: string;
|
||||
new?: number;
|
||||
parent_task_id?: string | null;
|
||||
status_id?: string;
|
||||
status_name?: string;
|
||||
phase_id?: string;
|
||||
phase_name?: string;
|
||||
phases?: IProjectTaskPhase[];
|
||||
labels?: IProjectTemplateLabel[];
|
||||
task_no?: number;
|
||||
original_task_id?: string;
|
||||
}
|
||||
|
||||
export interface ITaskIncludes {
|
||||
status?: boolean;
|
||||
phase?: boolean;
|
||||
labels?: boolean;
|
||||
estimation?: boolean;
|
||||
description?: boolean;
|
||||
subtasks?: boolean;
|
||||
}
|
||||
|
||||
export interface ICustomProjectTemplate {
|
||||
name?: string;
|
||||
phase_label?: string;
|
||||
color_code?: string;
|
||||
notes?: string;
|
||||
team_id?: string;
|
||||
}
|
||||
|
||||
export interface ICustomTemplatePhase {
|
||||
name?: string;
|
||||
color_code?: string;
|
||||
template_id?: string;
|
||||
}
|
||||
|
||||
export interface ICustomTemplateTask {
|
||||
name?: string;
|
||||
description: string;
|
||||
total_minutes: string;
|
||||
sort_order: string;
|
||||
priority_id: string;
|
||||
template_id: string;
|
||||
parent_task_id: string;
|
||||
status_id?: string;
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
import { Socket } from "socket.io";
|
||||
import db from "../../config/db";
|
||||
import HandleExceptions from "../../decorators/handle-exceptions";
|
||||
import { logStatusChange } from "../../services/activity-logs/activity-logs.service";
|
||||
import { getColor, int, log_error } from "../../shared/utils";
|
||||
import { generateProjectKey } from "../../utils/generate-project-key";
|
||||
import WorklenzControllerBase from "../worklenz-controller-base";
|
||||
import { ICustomProjectTemplate, ICustomTemplatePhase, IProjectTemplate, IProjectTemplateLabel, IProjectTemplatePhase, IProjectTemplateStatus, IProjectTemplateTask, ITaskIncludes } from "./interfaces";
|
||||
|
||||
export default abstract class ProjectTemplatesControllerBase extends WorklenzControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertProjectTemplate(body: IProjectTemplate) {
|
||||
const { name, key, description, phase_label } = body;
|
||||
|
||||
const q = `INSERT INTO pt_project_templates(name, key, description, phase_label) VALUES ($1, $2, $3, $4) RETURNING id;`;
|
||||
const result = await db.query(q, [name, key, description, phase_label]);
|
||||
const [data] = result.rows;
|
||||
return data.id;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertTemplateProjectPhases(body: IProjectTemplatePhase[], template_id: string) {
|
||||
for await (const phase of body) {
|
||||
const { name, color_code } = phase;
|
||||
|
||||
const q = `INSERT INTO pt_phases(name, color_code, template_id) VALUES ($1, $2, $3);`;
|
||||
await db.query(q, [name, color_code, template_id]);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertTemplateProjectStatuses(body: IProjectTemplateStatus[], template_id: string) {
|
||||
for await (const status of body) {
|
||||
const { name, category_name, category_id } = status;
|
||||
|
||||
const q = `INSERT INTO pt_statuses(name, template_id, category_id)
|
||||
VALUES ($1, $2, (SELECT id FROM sys_task_status_categories WHERE sys_task_status_categories.name = $3));`;
|
||||
await db.query(q, [name, template_id, category_name]);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertTemplateProjectTasks(body: IProjectTemplateTask[], template_id: string) {
|
||||
for await (const template_task of body) {
|
||||
const { name, description, total_minutes, sort_order, priority_name, parent_task_id, phase_name, status_name } = template_task;
|
||||
|
||||
const q = `INSERT INTO pt_tasks(name, description, total_minutes, sort_order, priority_id, template_id, parent_task_id, status_id)
|
||||
VALUES ($1, $2, $3, $4, (SELECT id FROM task_priorities WHERE task_priorities.name = $5), $6, $7,
|
||||
(SELECT id FROM pt_statuses WHERE pt_statuses.name = $8 AND pt_statuses.template_id = $6)) RETURNING id;`;
|
||||
const result = await db.query(q, [name, description, total_minutes, sort_order, priority_name, template_id, parent_task_id, status_name]);
|
||||
const [task] = result.rows;
|
||||
|
||||
await this.insertTemplateTaskPhases(task.id, template_id, phase_name);
|
||||
if (template_task.labels) await this.insertTemplateTaskLabels(task.id, template_task.labels);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertTemplateTaskPhases(task_id: string, template_id: string, phase_name = "") {
|
||||
const q = `INSERT INTO pt_task_phases (task_id, phase_id) VALUES ($1, (SELECT id FROM pt_phases WHERE template_id = $2 AND name = $3));`;
|
||||
await db.query(q, [task_id, template_id, phase_name]);
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertTemplateTaskLabels(task_id: string, labels: IProjectTemplateLabel[]) {
|
||||
for await (const label of labels) {
|
||||
const q = `INSERT INTO pt_task_labels(task_id, label_id) VALUES ($1, (SELECT id FROM pt_labels WHERE name = $2));`;
|
||||
await db.query(q, [task_id, label.name]);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async getTemplateData(template_id: string) {
|
||||
const q = `SELECT id,
|
||||
name,
|
||||
description,
|
||||
phase_label,
|
||||
image_url,
|
||||
color_code,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT name, color_code FROM pt_phases WHERE template_id = pt.id) rec) AS phases,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT name,
|
||||
category_id,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE sys_task_status_categories.id = pt_statuses.category_id)
|
||||
FROM pt_statuses
|
||||
WHERE template_id = pt.id) rec) AS status,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT name, pt_labels.color_code
|
||||
FROM pt_labels
|
||||
WHERE id IN (SELECT label_id
|
||||
FROM pt_task_labels pttl
|
||||
WHERE task_id IN (SELECT id
|
||||
FROM pt_tasks
|
||||
WHERE pt_tasks.template_id = pt.id))) rec) AS labels,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT name,
|
||||
color_code
|
||||
FROM task_priorities) rec) AS priorities,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT name,
|
||||
(SELECT name FROM pt_statuses WHERE status_id = pt_statuses.id) AS status_name,
|
||||
(SELECT name FROM task_priorities tp WHERE priority_id = tp.id ) AS priority_name,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT name
|
||||
FROM pt_phases pl
|
||||
WHERE pl.id =
|
||||
(SELECT phase_id FROM pt_task_phases WHERE task_id = pt_tasks.id)) rec) AS phases,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT name
|
||||
FROM pt_labels pl
|
||||
LEFT JOIN pt_task_labels pttl ON pl.id = pttl.label_id
|
||||
WHERE pttl.task_id = pt_tasks.id) rec) AS labels
|
||||
FROM pt_tasks
|
||||
WHERE template_id = pt.id) rec) AS tasks
|
||||
FROM pt_project_templates pt
|
||||
WHERE id = $1;`;
|
||||
const result = await db.query(q, [template_id]);
|
||||
const [data] = result.rows;
|
||||
for (const phase of data.phases) {
|
||||
phase.color_code = getColor(phase.name);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async getCustomTemplateData(template_id: string) {
|
||||
const q = `SELECT id,
|
||||
name,
|
||||
notes AS description,
|
||||
phase_label,
|
||||
color_code,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT name, color_code FROM cpt_phases WHERE template_id = pt.id) rec) AS phases,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT name,
|
||||
category_id,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE sys_task_status_categories.id = cpts.category_id)
|
||||
FROM cpt_task_statuses cpts
|
||||
WHERE template_id = pt.id ORDER BY sort_order) rec) AS status,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT name, tl.color_code
|
||||
FROM team_labels tl
|
||||
WHERE id IN (SELECT label_id
|
||||
FROM cpt_task_labels ctl
|
||||
WHERE task_id IN (SELECT id
|
||||
FROM cpt_tasks
|
||||
WHERE cpt_tasks.template_id = pt.id))) rec) AS labels,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT name,
|
||||
color_code
|
||||
FROM task_priorities) rec) AS priorities,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT id AS original_task_id,
|
||||
name,
|
||||
parent_task_id,
|
||||
description,
|
||||
total_minutes,
|
||||
(SELECT name FROM cpt_task_statuses cts WHERE status_id = cts.id) AS status_name,
|
||||
(SELECT name FROM task_priorities tp WHERE priority_id = tp.id) AS priority_name,
|
||||
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT name
|
||||
FROM cpt_phases pl
|
||||
WHERE pl.id =
|
||||
(SELECT phase_id FROM cpt_task_phases WHERE task_id = cpt_tasks.id)) rec) AS phases,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT name
|
||||
FROM team_labels pl
|
||||
LEFT JOIN cpt_task_labels cttl ON pl.id = cttl.label_id
|
||||
WHERE cttl.task_id = cpt_tasks.id) rec) AS labels
|
||||
FROM cpt_tasks
|
||||
WHERE template_id = pt.id
|
||||
ORDER BY parent_task_id NULLS FIRST) rec) AS tasks
|
||||
FROM custom_project_templates pt
|
||||
WHERE id = $1;`;
|
||||
const result = await db.query(q, [template_id]);
|
||||
const [data] = result.rows;
|
||||
return data;
|
||||
}
|
||||
|
||||
private static async getAllKeysByTeamId(teamId?: string) {
|
||||
if (!teamId) return [];
|
||||
try {
|
||||
const result = await db.query("SELECT key FROM projects WHERE team_id = $1;", [teamId]);
|
||||
return result.rows.map((project: any) => project.key).filter((key: any) => !!key);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static async checkProjectNameExists(project_name: string, teamId?: string) {
|
||||
if (!teamId) return;
|
||||
try {
|
||||
const result = await db.query("SELECT count(*) FROM projects WHERE name = $1 AND team_id = $2;", [project_name, teamId]);
|
||||
const [data] = result.rows;
|
||||
return int(data.count) || 0;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async importTemplate(body: any) {
|
||||
const q = `SELECT create_project($1) AS project`;
|
||||
|
||||
const count = await this.checkProjectNameExists(body.name, body.team_id);
|
||||
|
||||
const keys = await this.getAllKeysByTeamId(body.team_id as string);
|
||||
body.key = generateProjectKey(body.name, keys) || null;
|
||||
|
||||
if (count !== 0) body.name = `${body.name} - ${body.key}`;
|
||||
|
||||
const result = await db.query(q, [JSON.stringify(body)]);
|
||||
const [data] = result.rows;
|
||||
|
||||
return data.project.id;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertTeamLabels(labels: IProjectTemplateLabel[], team_id = "") {
|
||||
if (!team_id) return;
|
||||
|
||||
for await (const label of labels) {
|
||||
const q = `INSERT INTO team_labels(name, color_code, team_id)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (name, team_id) DO NOTHING;`;
|
||||
await db.query(q, [label.name, label.color_code, team_id]);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertProjectPhases(phases: IProjectTemplatePhase[], project_id = "",) {
|
||||
if (!project_id) return;
|
||||
|
||||
let i = 0;
|
||||
|
||||
for await (const phase of phases) {
|
||||
const q = `INSERT INTO project_phases(name, color_code, project_id, sort_index) VALUES ($1, $2, $3, $4);`;
|
||||
await db.query(q, [phase.name, phase.color_code, project_id, i]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
protected static async insertProjectStatuses(statuses: IProjectTemplateStatus[], project_id = "", team_id = "") {
|
||||
if (!project_id || !team_id) return;
|
||||
|
||||
try {
|
||||
for await (const status of statuses) {
|
||||
const q = `INSERT INTO task_statuses(name, project_id, team_id, category_id) VALUES($1, $2, $3, $4);`;
|
||||
await db.query(q, [status.name, project_id, team_id, status.category_id]);
|
||||
}
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertTaskPhase(task_id: string, phase_name: string, project_id: string) {
|
||||
const q = `INSERT INTO task_phase(task_id, phase_id)
|
||||
VALUES ($1, (SELECT id FROM project_phases WHERE name = $2 AND project_id = $3));`;
|
||||
await db.query(q, [task_id, phase_name, project_id]);
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertTaskLabel(task_id: string, label_name: string, team_id: string) {
|
||||
const q = `INSERT INTO task_labels(task_id, label_id)
|
||||
VALUES ($1, (SELECT id FROM team_labels WHERE name = $2 AND team_id = $3));`;
|
||||
await db.query(q, [task_id, label_name, team_id]);
|
||||
}
|
||||
|
||||
protected static async insertProjectTasks(tasks: IProjectTemplateTask[], team_id: string, project_id = "", user_id = "", socket: Socket | null) {
|
||||
if (!project_id) return;
|
||||
|
||||
try {
|
||||
for await (const [key, task] of tasks.entries()) {
|
||||
const q = `INSERT INTO tasks(name, project_id, status_id, priority_id, reporter_id, sort_order)
|
||||
VALUES ($1, $2, (SELECT id FROM task_statuses ts WHERE ts.name = $3 AND ts.project_id = $2),
|
||||
(SELECT id FROM task_priorities tp WHERE tp.name = $4), $5, $6)
|
||||
RETURNING id, status_id;`;
|
||||
const result = await db.query(q, [task.name, project_id, task.status_name, task.priority_name, user_id, key]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (task.phases) {
|
||||
for await (const phase of task.phases) {
|
||||
await this.insertTaskPhase(data.id, phase.name as string, project_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (task.labels) {
|
||||
for await (const label of task.labels) {
|
||||
await this.insertTaskLabel(data.id, label.name as string, team_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (socket) {
|
||||
logStatusChange({
|
||||
task_id: data.id,
|
||||
socket,
|
||||
new_value: data.status_id,
|
||||
old_value: null
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// custom templates
|
||||
@HandleExceptions()
|
||||
protected static async getProjectData(project_id: string) {
|
||||
const q = `SELECT phase_label, notes, color_code FROM projects WHERE id = $1;`;
|
||||
const result = await db.query(q, [project_id]);
|
||||
const [data] = result.rows;
|
||||
return data;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async getProjectStatus(project_id: string) {
|
||||
const q = `SELECT name, category_id, sort_order FROM task_statuses WHERE project_id = $1;`;
|
||||
const result = await db.query(q, [project_id]);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async getProjectPhases(project_id: string) {
|
||||
const q = `SELECT name, color_code FROM project_phases WHERE project_id = $1 ORDER BY sort_index ASC;`;
|
||||
const result = await db.query(q, [project_id]);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async getProjectLabels(team_id: string, project_id: string) {
|
||||
const q = `SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(DISTINCT JSONB_BUILD_OBJECT('name', name))), '[]'::JSON) AS labels
|
||||
FROM team_labels
|
||||
WHERE team_id = $1
|
||||
AND id IN (SELECT label_id
|
||||
FROM task_labels
|
||||
WHERE task_id IN (SELECT id
|
||||
FROM tasks
|
||||
WHERE project_id = $2));`;
|
||||
const result = await db.query(q, [team_id, project_id]);
|
||||
const [data] = result.rows;
|
||||
return data.labels;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async getTasksByProject(project_id: string, taskIncludes: ITaskIncludes) {
|
||||
let taskIncludesClause = "";
|
||||
|
||||
if (taskIncludes.description) taskIncludesClause += " description,";
|
||||
if (taskIncludes.estimation) taskIncludesClause += " total_minutes,";
|
||||
if (taskIncludes.status) taskIncludesClause += ` (SELECT name FROM task_statuses WHERE status_id = id) AS status_name,`;
|
||||
if (taskIncludes.labels) {
|
||||
taskIncludesClause += ` (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT (SELECT name FROM team_labels WHERE id = task_labels.label_id)
|
||||
FROM task_labels
|
||||
WHERE task_id = t.id) rec) AS labels,`;
|
||||
}
|
||||
if (taskIncludes.phase) {
|
||||
taskIncludesClause += ` (SELECT name
|
||||
FROM project_phases
|
||||
WHERE project_phases.id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_name,`;
|
||||
}
|
||||
if (taskIncludes.subtasks) {
|
||||
taskIncludesClause += ` parent_task_id,`;
|
||||
}
|
||||
|
||||
const q = `SELECT id,
|
||||
name,
|
||||
sort_order,
|
||||
task_no,
|
||||
${taskIncludesClause}
|
||||
priority_id
|
||||
FROM tasks t
|
||||
WHERE project_id = $1
|
||||
AND archived IS FALSE ORDER BY parent_task_id NULLS FIRST;`;
|
||||
const result = await db.query(q, [project_id]);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertCustomTemplate(body: ICustomProjectTemplate) {
|
||||
const q = `SELECT create_project_template($1)`;
|
||||
const result = await db.query(q, [JSON.stringify(body)]);
|
||||
const [data] = result.rows;
|
||||
return data.id;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertCustomTemplatePhases(body: ICustomTemplatePhase[], template_id: string) {
|
||||
for await (const phase of body) {
|
||||
const { name, color_code } = phase;
|
||||
|
||||
const q = `INSERT INTO cpt_phases(name, color_code, template_id) VALUES ($1, $2, $3);`;
|
||||
await db.query(q, [name, color_code, template_id]);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertCustomTemplateStatus(body: IProjectTemplateStatus[], template_id: string, team_id: string) {
|
||||
for await (const status of body) {
|
||||
const { name, category_id, sort_order } = status;
|
||||
|
||||
const q = `INSERT INTO cpt_task_statuses(name, template_id, team_id, category_id, sort_order)
|
||||
VALUES ($1, $2, $3, $4, $5);`;
|
||||
await db.query(q, [name, template_id, team_id, category_id, sort_order]);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertCustomTemplateTasks(body: IProjectTemplateTask[], template_id: string, team_id: string, status = true) {
|
||||
for await (const task of body) {
|
||||
const { name, description, total_minutes, sort_order, priority_id, status_name, task_no, parent_task_id, id, phase_name } = task;
|
||||
|
||||
const q = `INSERT INTO cpt_tasks(name, description, total_minutes, sort_order, priority_id, template_id, status_id, task_no,
|
||||
parent_task_id, original_task_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, (SELECT id FROM cpt_task_statuses cts WHERE cts.name = $7 AND cts.template_id = $6), $8,
|
||||
(SELECT id FROM cpt_tasks WHERE original_task_id = $9 AND template_id = $6), $10)
|
||||
RETURNING id;`;
|
||||
const result = await db.query(q, [name, description, total_minutes || 0, sort_order, priority_id, template_id, status_name, task_no, parent_task_id, id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (data.id) {
|
||||
if (phase_name) await this.insertCustomTemplateTaskPhases(data.id, template_id, phase_name);
|
||||
if (task.labels) await this.insertCustomTemplateTaskLabels(data.id, task.labels, team_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertCustomTemplateTaskPhases(task_id: string, template_id: string, phase_name = "") {
|
||||
const q = `INSERT INTO cpt_task_phases (task_id, phase_id)
|
||||
VALUES ($1, (SELECT id FROM cpt_phases WHERE template_id = $2 AND name = $3));`;
|
||||
await db.query(q, [task_id, template_id, phase_name]);
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async insertCustomTemplateTaskLabels(task_id: string, labels: IProjectTemplateLabel[], team_id: string) {
|
||||
for await (const label of labels) {
|
||||
const q = `INSERT INTO cpt_task_labels(task_id, label_id)
|
||||
VALUES ($1, (SELECT id FROM team_labels WHERE name = $2 AND team_id = $3));`;
|
||||
await db.query(q, [task_id, label.name, team_id]);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async updateTeamName(name: string, team_id: string, user_id: string) {
|
||||
const q = `UPDATE teams SET name = TRIM($1::TEXT) WHERE id = $2 AND user_id = $3;`;
|
||||
const result = await db.query(q, [name, team_id, user_id]);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async deleteDefaultStatusForProject(task_id: string) {
|
||||
const q = `DELETE FROM task_statuses WHERE project_id = $1;`;
|
||||
await db.query(q, [task_id]);
|
||||
}
|
||||
|
||||
|
||||
@HandleExceptions()
|
||||
protected static async handleAccountSetup(project_id: string, user_id: string, team_name: string) {
|
||||
// update user setup status
|
||||
await db.query(`UPDATE users SET setup_completed = TRUE WHERE id = $1;`, [user_id]);
|
||||
|
||||
await db.query(`INSERT INTO organizations (user_id, organization_name, contact_number, contact_number_secondary, trial_in_progress,
|
||||
trial_expire_date, subscription_status)
|
||||
VALUES ($1, TRIM($2::TEXT), NULL, NULL, TRUE, CURRENT_DATE + INTERVAL '14 days', 'trialing')
|
||||
ON CONFLICT (user_id) DO UPDATE SET organization_name = TRIM($2::TEXT);`, [user_id, team_name]);
|
||||
}
|
||||
|
||||
protected static async insertProjectTasksFromCustom(tasks: IProjectTemplateTask[], team_id: string, project_id = "", user_id = "", socket: Socket | null) {
|
||||
if (!project_id) return;
|
||||
|
||||
try {
|
||||
for await (const [key, task] of tasks.entries()) {
|
||||
const q = `INSERT INTO tasks(name, project_id, status_id, priority_id, reporter_id, sort_order, parent_task_id, description, total_minutes)
|
||||
VALUES ($1, $2, (SELECT id FROM task_statuses ts WHERE ts.name = $3 AND ts.project_id = $2),
|
||||
(SELECT id FROM task_priorities tp WHERE tp.name = $4), $5, $6, $7, $8, $9)
|
||||
RETURNING id, status_id;`;
|
||||
|
||||
const parent_task: IProjectTemplateTask = tasks.find(t => t.original_task_id === task.parent_task_id) || {};
|
||||
|
||||
const result = await db.query(q, [task.name, project_id, task.status_name, task.priority_name, user_id, key, parent_task.id, task.description, task.total_minutes ? task.total_minutes : 0]);
|
||||
const [data] = result.rows;
|
||||
task.id = data.id;
|
||||
|
||||
if (task.phases) {
|
||||
for await (const phase of task.phases) {
|
||||
await this.insertTaskPhase(data.id, phase.name as string, project_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (task.labels) {
|
||||
for await (const label of task.labels) {
|
||||
await this.insertTaskLabel(data.id, label.name as string, team_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (socket) {
|
||||
logStatusChange({
|
||||
task_id: data.id,
|
||||
socket,
|
||||
new_value: data.status_id,
|
||||
old_value: null
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,104 @@
|
||||
import db from "../../config/db";
|
||||
import HandleExceptions from "../../decorators/handle-exceptions";
|
||||
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
|
||||
import { ServerResponse } from "../../models/server-response";
|
||||
import { TASK_STATUS_COLOR_ALPHA } from "../../shared/constants";
|
||||
import { getColor } from "../../shared/utils";
|
||||
import WorklenzControllerBase from "../worklenz-controller-base";
|
||||
|
||||
export default class PtTaskPhasesController extends WorklenzControllerBase {
|
||||
|
||||
private static readonly DEFAULT_PHASE_COLOR = "#fbc84c";
|
||||
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
if (!req.query.id)
|
||||
return res.status(400).send(new ServerResponse(false, null, "Invalid request"));
|
||||
|
||||
const q = `
|
||||
INSERT INTO cpt_phases (name, color_code, template_id)
|
||||
VALUES (CONCAT('Untitled Phase (', (SELECT COUNT(*) FROM cpt_phases WHERE template_id = $2) + 1, ')'), $1,
|
||||
$2)
|
||||
RETURNING id, name, color_code;
|
||||
`;
|
||||
|
||||
req.body.color_code = this.DEFAULT_PHASE_COLOR;
|
||||
|
||||
const result = await db.query(q, [req.body.color_code, req.query.id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
data.color_code = data.color_code + TASK_STATUS_COLOR_ALPHA;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT id, name, color_code, (SELECT COUNT(*) FROM cpt_task_phases WHERE phase_id = cpt_phases.id) AS usage
|
||||
FROM cpt_phases
|
||||
WHERE template_id = $1
|
||||
ORDER BY created_at DESC;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [req.query.id]);
|
||||
|
||||
for (const phase of result.rows)
|
||||
phase.color_code = phase.color_code + TASK_STATUS_COLOR_ALPHA;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
|
||||
@HandleExceptions({
|
||||
raisedExceptions: {
|
||||
"PHASE_EXISTS_ERROR": `Phase name "{0}" already exists. Please choose a different name.`
|
||||
}
|
||||
})
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT update_phase_name($1, $2, $3);`;
|
||||
|
||||
const result = await db.query(q, [req.params.id, req.body.name.trim(), req.query.id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
data.update_phase_name.color_code = data.update_phase_name.color_code + TASK_STATUS_COLOR_ALPHA;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.update_phase_name));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateLabel(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
UPDATE custom_project_templates
|
||||
SET phase_label = $2
|
||||
WHERE id = $1;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [req.params.id, req.body.name.trim()]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateColor(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `UPDATE cpt_phases SET color_code = $3 WHERE id = $1 AND template_id = $2 RETURNING id, name, color_code;`;
|
||||
const result = await db.query(q, [req.params.id, req.query.id, req.body.color_code.substring(0, req.body.color_code.length - 2)]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
DELETE
|
||||
FROM cpt_phases
|
||||
WHERE id = $1
|
||||
AND template_id = $2
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [req.params.id, req.query.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import db from "../../config/db";
|
||||
import HandleExceptions from "../../decorators/handle-exceptions";
|
||||
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
|
||||
import { ServerResponse } from "../../models/server-response";
|
||||
import WorklenzControllerBase from "../worklenz-controller-base";
|
||||
|
||||
const existsErrorMessage = "At least one status should exists under each category.";
|
||||
|
||||
export default class PtTaskStatusesController extends WorklenzControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
INSERT INTO cpt_task_statuses (name, template_id, team_id, category_id, sort_order)
|
||||
VALUES ($1, $2, $3, $4, (SELECT MAX(sort_order) FROM cpt_task_statuses WHERE template_id = $2) + 1);
|
||||
`;
|
||||
const result = await db.query(q, [req.body.name, req.body.template_id, req.user?.team_id, req.body.category_id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getCreated(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const team_id = req.user?.team_id;
|
||||
const q = `SELECT create_pt_task_status($1, $2)`;
|
||||
const result = await db.query(q, [JSON.stringify(req.body), team_id]);
|
||||
const data = result.rows[0].create_pt_task_status[0];
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
if (!req.query.template_id)
|
||||
return res.status(400).send(new ServerResponse(false, null));
|
||||
|
||||
const q = `
|
||||
SELECT cpt_task_statuses.id,
|
||||
cpt_task_statuses.name,
|
||||
stsc.color_code,
|
||||
stsc.name AS category_name,
|
||||
cpt_task_statuses.category_id,
|
||||
stsc.description
|
||||
FROM cpt_task_statuses
|
||||
INNER JOIN sys_task_status_categories stsc ON cpt_task_statuses.category_id = stsc.id
|
||||
WHERE template_id = $1
|
||||
AND team_id = $2
|
||||
ORDER BY cpt_task_statuses.sort_order;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [req.query.template_id, req.user?.team_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getCategories(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name, color_code, description
|
||||
FROM sys_task_status_categories
|
||||
ORDER BY index;`;
|
||||
const result = await db.query(q, []);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT cpt_task_statuses.id, cpt_task_statuses.name, stsc.color_code
|
||||
FROM cpt_task_statuses
|
||||
INNER JOIN sys_task_status_categories stsc ON cpt_task_statuses.category_id = stsc.id
|
||||
WHERE cpt_task_statuses.id = $1
|
||||
AND template_id = $2;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.query.template_id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
private static async hasMoreCategories(statusId: string, templateId: string) {
|
||||
if (!statusId || !templateId)
|
||||
return false;
|
||||
|
||||
const q = `
|
||||
SELECT COUNT(*) AS count
|
||||
FROM cpt_task_statuses
|
||||
WHERE category_id = (SELECT category_id FROM cpt_task_statuses WHERE id = $1)
|
||||
AND template_id = $2;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [statusId, templateId]);
|
||||
const [data] = result.rows;
|
||||
return +data.count >= 2;
|
||||
}
|
||||
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const hasMoreCategories = await PtTaskStatusesController.hasMoreCategories(req.params.id, req.body.template_id);
|
||||
|
||||
if (!hasMoreCategories)
|
||||
return res.status(200).send(new ServerResponse(false, null, existsErrorMessage).withTitle("Status update failed!"));
|
||||
|
||||
const q = `
|
||||
UPDATE cpt_task_statuses
|
||||
SET name = $2,
|
||||
category_id = COALESCE($4, (SELECT id FROM sys_task_status_categories WHERE is_todo IS TRUE))
|
||||
WHERE id = $1
|
||||
AND template_id = $3
|
||||
RETURNING (SELECT color_code FROM sys_task_status_categories WHERE id = cpt_task_statuses.category_id);
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.body.name, req.body.template_id, req.body.category_id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions({
|
||||
raisedExceptions: {
|
||||
"STATUS_EXISTS_ERROR": `Status name "{0}" already exists. Please choose a different name.`
|
||||
}
|
||||
})
|
||||
public static async updateName(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `DO
|
||||
$$
|
||||
BEGIN
|
||||
-- check whether the status name is already in
|
||||
IF EXISTS(SELECT name
|
||||
FROM cpt_task_statuses
|
||||
WHERE name = '${req.body.name}'::TEXT
|
||||
AND template_id = '${req.body.template_id}'::UUID)
|
||||
THEN
|
||||
RAISE 'STATUS_EXISTS_ERROR:%', ('${req.body.name}')::TEXT;
|
||||
END IF;
|
||||
|
||||
UPDATE cpt_task_statuses
|
||||
SET name = '${req.body.name}'::TEXT
|
||||
WHERE id = '${req.params.id}'::UUID
|
||||
AND template_id = '${req.body.template_id}'::UUID;
|
||||
END
|
||||
$$;`;
|
||||
const result = await db.query(q, []);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import WorklenzControllerBase from "../worklenz-controller-base";
|
||||
import { getColor } from "../../shared/utils";
|
||||
import { PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA } from "../../shared/constants";
|
||||
|
||||
export const GroupBy = {
|
||||
STATUS: "status",
|
||||
PRIORITY: "priority",
|
||||
LABELS: "labels",
|
||||
PHASE: "phase"
|
||||
};
|
||||
|
||||
export interface ITaskGroup {
|
||||
id?: string;
|
||||
name: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
color_code: string;
|
||||
category_id: string | null;
|
||||
old_category_id?: string;
|
||||
todo_progress?: number;
|
||||
doing_progress?: number;
|
||||
done_progress?: number;
|
||||
tasks: any[];
|
||||
}
|
||||
|
||||
|
||||
export default class PtTasksControllerBase extends WorklenzControllerBase {
|
||||
|
||||
public static updateTaskViewModel(task: any) {
|
||||
|
||||
task.time_spent = {hours: ~~(task.total_minutes_spent / 60), minutes: task.total_minutes_spent % 60};
|
||||
|
||||
if (typeof task.sub_tasks_count === "undefined") task.sub_tasks_count = "0";
|
||||
|
||||
task.is_sub_task = !!task.parent_task_id;
|
||||
|
||||
task.total_time_string = `${~~(task.total_minutes / 60)}h ${(task.total_minutes % 60)}m`;
|
||||
|
||||
task.priority_color = PriorityColorCodes[task.priority_value] || PriorityColorCodes["0"];
|
||||
task.show_sub_tasks = false;
|
||||
|
||||
if (task.phase_id) {
|
||||
task.phase_color = task.phase_color
|
||||
? task.phase_color + TASK_PRIORITY_COLOR_ALPHA : getColor(task.phase_name) + TASK_PRIORITY_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
task.all_labels = task.labels;
|
||||
task.labels = PtTasksControllerBase.createTagList(task.labels, 2);
|
||||
|
||||
task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
|
||||
task.priority_color = task.priority_color + TASK_PRIORITY_COLOR_ALPHA;
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import { ParsedQs } from "qs";
|
||||
|
||||
import db from "../../config/db";
|
||||
import HandleExceptions from "../../decorators/handle-exceptions";
|
||||
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
|
||||
import { ServerResponse } from "../../models/server-response";
|
||||
import { TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA, UNMAPPED } from "../../shared/constants";
|
||||
import { getColor } from "../../shared/utils";
|
||||
import PtTasksControllerBase, { GroupBy, ITaskGroup } from "./pt-tasks-controller-base";
|
||||
|
||||
export class PtTaskListGroup implements ITaskGroup {
|
||||
name: string;
|
||||
category_id: string | null;
|
||||
color_code: string;
|
||||
tasks: any[];
|
||||
|
||||
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 = [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class PtTasksController extends PtTasksControllerBase {
|
||||
private static isCountsOnly(query: ParsedQs) {
|
||||
return query.count === "true";
|
||||
}
|
||||
|
||||
public static isTasksOnlyReq(query: ParsedQs) {
|
||||
return PtTasksController.isCountsOnly(query) || query.parent_task;
|
||||
}
|
||||
|
||||
private static flatString(text: string) {
|
||||
return (text || "").split(" ").map(s => `'${s}'`).join(",");
|
||||
}
|
||||
|
||||
private static getFilterByTemplatsWhereClosure(text: string) {
|
||||
return text ? `template_id IN (${this.flatString(text)})` : "";
|
||||
}
|
||||
|
||||
private static getQuery(userId: string, options: ParsedQs) {
|
||||
|
||||
const searchField = options.search ? "cptt.name" : "sort_order";
|
||||
const { searchQuery, sortField } = PtTasksController.toPaginationOptions(options, searchField);
|
||||
|
||||
const sortFields = sortField.replace(/ascend/g, "ASC").replace(/descend/g, "DESC") || "sort_order";
|
||||
|
||||
const isSubTasks = !!options.parent_task;
|
||||
|
||||
const subTasksFilter = isSubTasks ? "parent_task_id = $2" : "parent_task_id IS NULL";
|
||||
|
||||
return `
|
||||
SELECT id,
|
||||
name,
|
||||
cptt.template_id AS template_id,
|
||||
cptt.parent_task_id,
|
||||
cptt.parent_task_id IS NOT NULL AS is_sub_task,
|
||||
(SELECT COUNT(*)
|
||||
FROM cpt_tasks
|
||||
WHERE parent_task_id = cptt.id)::INT AS sub_tasks_count,
|
||||
cptt.status_id AS status,
|
||||
cptt.description,
|
||||
cptt.sort_order,
|
||||
(SELECT phase_id FROM cpt_task_phases WHERE task_id = cptt.id) AS phase_id,
|
||||
(SELECT name
|
||||
FROM cpt_phases
|
||||
WHERE id = (SELECT phase_id FROM cpt_task_phases WHERE task_id = cptt.id)) AS phase_name,
|
||||
(SELECT color_code
|
||||
FROM cpt_phases
|
||||
WHERE id = (SELECT phase_id FROM cpt_task_phases WHERE task_id = cptt.id)) AS phase_color,
|
||||
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM cpt_task_statuses WHERE id = cptt.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 cpt_task_statuses WHERE id = cptt.status_id)) r) AS status_category,
|
||||
|
||||
(SELECT COALESCE(JSON_AGG(r), '[]'::JSON)
|
||||
FROM (SELECT cpt_task_labels.label_id AS id,
|
||||
(SELECT name FROM team_labels WHERE id = cpt_task_labels.label_id),
|
||||
(SELECT color_code FROM team_labels WHERE id = cpt_task_labels.label_id)
|
||||
FROM cpt_task_labels
|
||||
WHERE task_id = cptt.id) r) AS labels,
|
||||
(SELECT id FROM task_priorities WHERE id = cptt.priority_id) AS priority,
|
||||
(SELECT value FROM task_priorities WHERE id = cptt.priority_id) AS priority_value,
|
||||
total_minutes
|
||||
FROM cpt_tasks cptt
|
||||
WHERE cptt.template_id=$1 AND ${subTasksFilter} ${searchQuery}
|
||||
ORDER BY ${sortFields}
|
||||
`;
|
||||
}
|
||||
|
||||
public static async getGroups(groupBy: string, templateId: string): Promise<ITaskGroup[]> {
|
||||
let q = "";
|
||||
let params: any[] = [];
|
||||
switch (groupBy) {
|
||||
case GroupBy.STATUS:
|
||||
q = `
|
||||
SELECT id,
|
||||
name,
|
||||
(SELECT color_code FROM sys_task_status_categories WHERE id = cpt_task_statuses.category_id),
|
||||
category_id
|
||||
FROM cpt_task_statuses
|
||||
WHERE template_id = $1
|
||||
ORDER BY sort_order;
|
||||
`;
|
||||
params = [templateId];
|
||||
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 cpt_tasks
|
||||
WHERE template_id = $1
|
||||
AND EXISTS(SELECT 1 FROM cpt_task_labels WHERE task_id = cpt_tasks.id AND label_id = team_labels.id))
|
||||
ORDER BY name;
|
||||
`;
|
||||
break;
|
||||
case GroupBy.PHASE:
|
||||
q = `
|
||||
SELECT id, name, color_code
|
||||
FROM cpt_phases
|
||||
WHERE template_id = $1
|
||||
ORDER BY created_at DESC;
|
||||
`;
|
||||
params = [templateId];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const result = await db.query(q, params);
|
||||
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 = PtTasksController.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]: ITaskGroup }, group) => {
|
||||
if (group.id)
|
||||
g[group.id] = new PtTaskListGroup(group);
|
||||
return g;
|
||||
}, {});
|
||||
|
||||
this.updateMapByGroup(tasks, groupBy, map);
|
||||
|
||||
const updatedGroups = Object.keys(map).map(key => {
|
||||
const group = map[key];
|
||||
|
||||
return {
|
||||
id: key,
|
||||
...group
|
||||
};
|
||||
});
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, updatedGroups));
|
||||
}
|
||||
|
||||
public static updateMapByGroup(tasks: any[], groupBy: string, map: { [p: string]: ITaskGroup }) {
|
||||
let index = 0;
|
||||
const unmapped = [];
|
||||
for (const task of tasks) {
|
||||
task.index = index++;
|
||||
PtTasksController.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);
|
||||
}
|
||||
|
||||
const totalMinutes = task.total_minutes;
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
|
||||
task.total_hours = hours;
|
||||
task.total_minutes = minutes;
|
||||
}
|
||||
|
||||
if (unmapped.length) {
|
||||
map[UNMAPPED] = {
|
||||
name: UNMAPPED,
|
||||
category_id: null,
|
||||
color_code: "#fbc84c69",
|
||||
tasks: unmapped
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTasksOnly(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const isSubTasks = !!req.query.parent_task;
|
||||
const q = PtTasksController.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) {
|
||||
PtTasksController.updateTaskViewModel(task);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async bulkDelete(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const deletedTasks = req.body.tasks.map((t: any) => t.id);
|
||||
|
||||
const result: any = {deleted_tasks: deletedTasks};
|
||||
|
||||
const q = `SELECT bulk_delete_pt_tasks($1) AS task;`;
|
||||
await db.query(q, [JSON.stringify(req.body)]);
|
||||
return res.status(200).send(new ServerResponse(true, result));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
|
||||
|
||||
import db from "../../config/db";
|
||||
import { ServerResponse } from "../../models/server-response";
|
||||
import HandleExceptions from "../../decorators/handle-exceptions";
|
||||
import { templateData } from "./project-templates";
|
||||
import ProjectTemplatesControllerBase from "./project-templates-base";
|
||||
import { LOG_DESCRIPTIONS, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA } from "../../shared/constants";
|
||||
import { IO } from "../../shared/io";
|
||||
|
||||
export default class ProjectTemplatesController extends ProjectTemplatesControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTemplates(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name FROM pt_project_templates ORDER BY name;`;
|
||||
const result = await db.query(q, []);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getCustomTemplates(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { searchQuery } = this.toPaginationOptions(req.query, "name");
|
||||
|
||||
const q = `SELECT id, name, created_at, FALSE AS selected FROM custom_project_templates WHERE team_id = $1 ${searchQuery} ORDER BY name;`;
|
||||
const result = await db.query(q, [req.user?.team_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteCustomTemplate(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
|
||||
const q = `DELETE FROM custom_project_templates WHERE id = $1;`;
|
||||
await db.query(q, [id]);
|
||||
return res.status(200).send(new ServerResponse(true, [], "Template deleted successfully."));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getDefaultProjectStatus() {
|
||||
const q = `SELECT id FROM sys_project_statuses WHERE is_default IS TRUE;`;
|
||||
const result = await db.query(q, []);
|
||||
const [data] = result.rows;
|
||||
return data.id;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getDefaultProjectHealth() {
|
||||
const q = `SELECT id FROM sys_project_healths WHERE is_default IS TRUE`;
|
||||
const result = await db.query(q, []);
|
||||
const [data] = result.rows;
|
||||
return data.id;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTemplateById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
const data = await this.getTemplateData(id);
|
||||
|
||||
for (const phase of data.phases) {
|
||||
phase.color_code = phase.color_code + TASK_STATUS_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
for (const status of data.status) {
|
||||
status.color_code = status.color_code + TASK_STATUS_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
for (const priority of data.priorities) {
|
||||
priority.color_code = priority.color_code + TASK_PRIORITY_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
for (const label of data.labels) {
|
||||
label.color_code = label.color_code + TASK_STATUS_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async createTemplates(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
for (const template of templateData) {
|
||||
let template_id: string | null = null;
|
||||
template_id = await this.insertProjectTemplate(template);
|
||||
if (template_id) {
|
||||
await this.insertTemplateProjectPhases(template.phases, template_id);
|
||||
await this.insertTemplateProjectStatuses(template.status, template_id);
|
||||
await this.insertTemplateProjectTasks(template.tasks, template_id);
|
||||
}
|
||||
}
|
||||
return res.status(200).send(new ServerResponse(true, []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async importTemplates(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { template_id } = req.body;
|
||||
let project_id: string | null = null;
|
||||
|
||||
const data = await this.getTemplateData(template_id);
|
||||
if (data) {
|
||||
data.team_id = req.user?.team_id || null;
|
||||
data.user_id = req.user?.id || null;
|
||||
data.folder_id = null;
|
||||
data.category_id = null;
|
||||
data.status_id = await this.getDefaultProjectStatus();
|
||||
data.project_created_log = LOG_DESCRIPTIONS.PROJECT_CREATED;
|
||||
data.project_member_added_log = LOG_DESCRIPTIONS.PROJECT_MEMBER_ADDED;
|
||||
data.health_id = await this.getDefaultProjectHealth();
|
||||
data.working_days = 0;
|
||||
data.man_days = 0;
|
||||
data.hours_per_day = 8;
|
||||
|
||||
project_id = await this.importTemplate(data);
|
||||
|
||||
await this.insertTeamLabels(data.labels, req.user?.team_id);
|
||||
await this.insertProjectPhases(data.phases, project_id as string);
|
||||
await this.insertProjectTasks(data.tasks, data.team_id, project_id as string, data.user_id, IO.getSocketById(req.user?.socket_id as string));
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, { project_id }));
|
||||
}
|
||||
return res.status(200).send(new ServerResponse(true, { project_id }));
|
||||
}
|
||||
|
||||
@HandleExceptions({
|
||||
raisedExceptions: {
|
||||
"TEMPLATE_EXISTS_ERROR": `A template with the name "{0}" already exists. Please choose a different name.`
|
||||
}
|
||||
})
|
||||
public static async createCustomTemplate(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { project_id, templateName, projectIncludes, taskIncludes } = req.body;
|
||||
const team_id = req.user?.team_id || null;
|
||||
|
||||
if (!team_id || !project_id) return res.status(400).send(new ServerResponse(false, {}));
|
||||
|
||||
|
||||
let status, labels, phases = [];
|
||||
|
||||
const data = await this.getProjectData(project_id);
|
||||
|
||||
if (projectIncludes.statuses) {
|
||||
status = await this.getProjectStatus(project_id);
|
||||
}
|
||||
if (projectIncludes.phases) {
|
||||
phases = await this.getProjectPhases(project_id);
|
||||
}
|
||||
if (projectIncludes.labels) {
|
||||
labels = await this.getProjectLabels(team_id, project_id);
|
||||
}
|
||||
|
||||
const tasks = await this.getTasksByProject(project_id, taskIncludes);
|
||||
|
||||
data.name = templateName;
|
||||
data.team_id = team_id;
|
||||
|
||||
const q = `SELECT create_project_template($1);`;
|
||||
const result = await db.query(q, [JSON.stringify(data)]);
|
||||
const [obj] = result.rows;
|
||||
|
||||
const template_id = obj.create_project_template.id;
|
||||
|
||||
if (template_id) {
|
||||
if (phases) await this.insertCustomTemplatePhases(phases, template_id);
|
||||
if (status) await this.insertCustomTemplateStatus(status, template_id, team_id);
|
||||
if (tasks) await this.insertCustomTemplateTasks(tasks, template_id, team_id);
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, {}, "Project template created successfully."));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async setupAccount(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { template_id, team_name } = req.body;
|
||||
let project_id: string | null = null;
|
||||
|
||||
await this.updateTeamName(team_name, req.user?.team_id as string, req.user?.id as string);
|
||||
|
||||
const data = await this.getTemplateData(template_id);
|
||||
if (data) {
|
||||
data.team_id = req.user?.team_id || null;
|
||||
data.user_id = req.user?.id || null;
|
||||
data.folder_id = null;
|
||||
data.category_id = null;
|
||||
data.status_id = await this.getDefaultProjectStatus();
|
||||
data.project_created_log = LOG_DESCRIPTIONS.PROJECT_CREATED;
|
||||
data.project_member_added_log = LOG_DESCRIPTIONS.PROJECT_MEMBER_ADDED;
|
||||
data.health_id = await this.getDefaultProjectHealth();
|
||||
data.working_days = 0;
|
||||
data.man_days = 0;
|
||||
data.hours_per_day = 8;
|
||||
|
||||
project_id = await this.importTemplate(data);
|
||||
|
||||
await this.insertTeamLabels(data.labels, req.user?.team_id);
|
||||
await this.insertProjectPhases(data.phases, project_id as string);
|
||||
await this.insertProjectTasks(data.tasks, data.team_id, project_id as string, data.user_id, IO.getSocketById(req.user?.socket_id as string));
|
||||
|
||||
await this.handleAccountSetup(project_id as string, data.user_id, team_name);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, { id: project_id }));
|
||||
}
|
||||
return res.status(200).send(new ServerResponse(true, { id: project_id }));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async importCustomTemplate(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { template_id } = req.body;
|
||||
let project_id: string | null = null;
|
||||
|
||||
const data = await this.getCustomTemplateData(template_id);
|
||||
if (data) {
|
||||
data.team_id = req.user?.team_id || null;
|
||||
data.user_id = req.user?.id || null;
|
||||
data.folder_id = null;
|
||||
data.category_id = null;
|
||||
data.status_id = await this.getDefaultProjectStatus();
|
||||
data.project_created_log = LOG_DESCRIPTIONS.PROJECT_CREATED;
|
||||
data.project_member_added_log = LOG_DESCRIPTIONS.PROJECT_MEMBER_ADDED;
|
||||
data.working_days = 0;
|
||||
data.man_days = 0;
|
||||
data.hours_per_day = 8;
|
||||
|
||||
project_id = await this.importTemplate(data);
|
||||
|
||||
await this.deleteDefaultStatusForProject(project_id as string);
|
||||
await this.insertTeamLabels(data.labels, req.user?.team_id);
|
||||
await this.insertProjectPhases(data.phases, project_id as string);
|
||||
await this.insertProjectStatuses(data.status, project_id as string, data.team_id );
|
||||
await this.insertProjectTasksFromCustom(data.tasks, data.team_id, project_id as string, data.user_id, IO.getSocketById(req.user?.socket_id as string));
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, { project_id }));
|
||||
}
|
||||
return res.status(200).send(new ServerResponse(true, { project_id }));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user