Initial commit: Angular frontend and Expressjs backend
This commit is contained in:
176
worklenz-backend/src/app.ts
Normal file
176
worklenz-backend/src/app.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import createError from "http-errors";
|
||||
import express, {NextFunction, Request, Response} from "express";
|
||||
import path from "path";
|
||||
import cookieParser from "cookie-parser";
|
||||
import logger from "morgan";
|
||||
import helmet from "helmet";
|
||||
import compression from "compression";
|
||||
import passport from "passport";
|
||||
import csurf from "csurf";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import cors from "cors";
|
||||
import uglify from "uglify-js";
|
||||
import flash from "connect-flash";
|
||||
import hpp from "hpp";
|
||||
|
||||
import passportConfig from "./passport";
|
||||
import indexRouter from "./routes/index";
|
||||
import apiRouter from "./routes/apis";
|
||||
import authRouter from "./routes/auth";
|
||||
import emailTemplatesRouter from "./routes/email-templates";
|
||||
|
||||
import public_router from "./routes/public";
|
||||
import {isInternalServer, isProduction} from "./shared/utils";
|
||||
import sessionMiddleware from "./middlewares/session-middleware";
|
||||
import {send_to_slack} from "./shared/slack";
|
||||
import {CSP_POLICIES} from "./shared/csp";
|
||||
import safeControllerFunction from "./shared/safe-controller-function";
|
||||
import AwsSesController from "./controllers/aws-ses-controller";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(compression());
|
||||
app.use(helmet({crossOriginResourcePolicy: false, crossOriginEmbedderPolicy: false}));
|
||||
|
||||
app.use((_req: Request, res: Response, next: NextFunction) => {
|
||||
res.setHeader("X-XSS-Protection", "1; mode=block");
|
||||
res.removeHeader("server");
|
||||
next();
|
||||
});
|
||||
|
||||
function isLoggedIn(req: Request, _res: Response, next: NextFunction) {
|
||||
return req.user ? next() : next(createError(401));
|
||||
}
|
||||
|
||||
passportConfig(passport);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require("pug").filters = {
|
||||
/**
|
||||
* ```pug
|
||||
* script
|
||||
* :minify_js
|
||||
* // JavaScript Syntax
|
||||
* ```
|
||||
* @param {String} text
|
||||
* @param {Object} options
|
||||
*/
|
||||
minify_js(text: string) {
|
||||
if (!text) return;
|
||||
// return text;
|
||||
return uglify.minify({"script.js": text}).code;
|
||||
}
|
||||
};
|
||||
|
||||
// view engine setup
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
app.set("view engine", "pug");
|
||||
|
||||
app.use(logger("dev"));
|
||||
app.use(express.json({limit: "50mb"}));
|
||||
app.use(express.urlencoded({extended: false, limit: "50mb"}));
|
||||
// Prevent HTTP Parameter Pollution
|
||||
app.use(hpp());
|
||||
app.use(cookieParser(process.env.COOKIE_SECRET));
|
||||
|
||||
app.use(cors({
|
||||
origin: [`https://${process.env.HOSTNAME}`],
|
||||
methods: "GET,PUT,POST,DELETE",
|
||||
preflightContinue: false,
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
app.post("/-/csp", (req: express.Request, res: express.Response) => {
|
||||
send_to_slack({
|
||||
type: "⚠️ CSP Report",
|
||||
body: req.body
|
||||
});
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.post("/webhook/emails/bounce", safeControllerFunction(AwsSesController.handleBounceResponse));
|
||||
app.post("/webhook/emails/complaints", safeControllerFunction(AwsSesController.handleComplaintResponse));
|
||||
app.post("/webhook/emails/reply", safeControllerFunction(AwsSesController.handleReplies));
|
||||
|
||||
app.use(flash());
|
||||
app.use(csurf({cookie: true}));
|
||||
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
res.setHeader("Content-Security-Policy", CSP_POLICIES);
|
||||
const token = req.csrfToken();
|
||||
res.cookie("XSRF-TOKEN", token);
|
||||
res.locals.csrf = token;
|
||||
next();
|
||||
});
|
||||
|
||||
if (isProduction()) {
|
||||
app.get("*.js", (req, res, next) => {
|
||||
if (req.header("Accept-Encoding")?.includes("br")) {
|
||||
req.url = `${req.url}.br`;
|
||||
res.set("Content-Encoding", "br");
|
||||
res.set("Content-Type", "application/javascript; charset=UTF-8");
|
||||
} else if (req.header("Accept-Encoding")?.includes("gzip")) {
|
||||
req.url = `${req.url}.gz`;
|
||||
res.set("Content-Encoding", "gzip");
|
||||
res.set("Content-Type", "application/javascript; charset=UTF-8");
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
app.set("trust proxy", 1);
|
||||
app.use(sessionMiddleware);
|
||||
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 1500, // Limit each IP to 2000 requests per `window` (here, per 15 minutes)
|
||||
standardHeaders: false, // Return rate limit info in the `RateLimit-*` headers
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||
});
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const {send} = res;
|
||||
res.send = function (obj) {
|
||||
if (req.headers.accept?.includes("application/json"))
|
||||
return send.call(this, `)]}',\n${JSON.stringify(obj)}`);
|
||||
return send.call(this, obj);
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
||||
app.use("/secure", authRouter);
|
||||
app.use("/public", public_router);
|
||||
app.use("/api/v1", isLoggedIn, apiRouter);
|
||||
app.use("/", indexRouter);
|
||||
|
||||
if (isInternalServer())
|
||||
app.use("/email-templates", emailTemplatesRouter);
|
||||
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use((req: Request, res: Response) => {
|
||||
res.locals.error_title = "404 Not Found.";
|
||||
res.locals.error_message = `The requested URL ${req.url} was not found on this server.`;
|
||||
res.locals.error_image = "/assets/images/404.webp";
|
||||
res.status(400);
|
||||
res.render("error");
|
||||
});
|
||||
|
||||
// error handler
|
||||
app.use((err: { message: string; status: number; }, _req: Request, res: Response) => {
|
||||
// set locals, only providing error in development
|
||||
res.locals.error_title = "500 Internal Server Error.";
|
||||
res.locals.error_message = "Oops, something went wrong.";
|
||||
res.locals.error_message2 = "Try to refresh this page or feel free to contact us if the problem persists.";
|
||||
res.locals.error_image = "/assets/images/500.png";
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render("error");
|
||||
});
|
||||
|
||||
export default app;
|
||||
6
worklenz-backend/src/bin/config.ts
Normal file
6
worklenz-backend/src/bin/config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import dotenv from "dotenv";
|
||||
import SegfaultHandler from "segfault-handler";
|
||||
|
||||
dotenv.config();
|
||||
global.Promise = require("bluebird");
|
||||
SegfaultHandler.registerHandler("crash.log");
|
||||
119
worklenz-backend/src/bin/www.ts
Normal file
119
worklenz-backend/src/bin/www.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// config should be imported at the top of this file.
|
||||
import "./config";
|
||||
|
||||
import {Server, Socket} from "socket.io";
|
||||
import http, {IncomingHttpHeaders} from "http";
|
||||
|
||||
import app from "../app";
|
||||
import {register} from "../socket.io";
|
||||
import {IO} from "../shared/io";
|
||||
import sessionMiddleware from "../middlewares/session-middleware";
|
||||
import {getLoggedInUserIdFromSocket} from "../socket.io/util";
|
||||
import {startCronJobs} from "../cron_jobs";
|
||||
import FileConstants from "../shared/file-constants";
|
||||
import {initRedis} from "../redis/client";
|
||||
import DbTaskStatusChangeListener from "../pg_notify_listeners/db-task-status-changed";
|
||||
|
||||
function normalizePort(val?: string) {
|
||||
const p = parseInt(val || "0", 10);
|
||||
if (isNaN(p)) return val; // named pipe
|
||||
if (p >= 0) return p; // port number
|
||||
return false;
|
||||
}
|
||||
|
||||
const port = normalizePort(process.env.PORT);
|
||||
app.set("port", port);
|
||||
|
||||
const server = http.createServer(app);
|
||||
|
||||
const io = new Server(server, {
|
||||
transports: ["websocket"],
|
||||
path: "/socket",
|
||||
cors: {
|
||||
origin: (process.env.SOCKET_IO_CORS || "*").split(",")
|
||||
},
|
||||
cookie: true
|
||||
});
|
||||
|
||||
const wrap = (middleware: any) => (socket: any, next: any) => middleware(socket.request, {}, next);
|
||||
|
||||
io.use(wrap(sessionMiddleware));
|
||||
|
||||
io.use((socket, next) => {
|
||||
const userId = getLoggedInUserIdFromSocket(socket);
|
||||
if (userId)
|
||||
return next();
|
||||
return next(new Error("401 unauthorized"));
|
||||
});
|
||||
|
||||
io.engine.on("initial_headers", (headers: IncomingHttpHeaders) => {
|
||||
headers["Strict-Transport-Security"] = "max-age=63072000; includeSubDomains";
|
||||
headers["X-Content-Type-Options"] = "nosniff";
|
||||
headers["X-Frame-Options"] = "Deny";
|
||||
headers["X-XSS-Protection"] = "1; mode=block";
|
||||
});
|
||||
|
||||
io.on("connection", (socket: Socket) => {
|
||||
register(io, socket);
|
||||
});
|
||||
|
||||
IO.setInstance(io);
|
||||
|
||||
function onError(error: any) {
|
||||
DbTaskStatusChangeListener.disconnect();
|
||||
|
||||
if (error.syscall !== "listen") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const bind = typeof port === "string"
|
||||
? `Pipe ${port}`
|
||||
: `Port ${port}`;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case "EACCES":
|
||||
console.error(`${bind} requires elevated privileges`);
|
||||
process.exit(1);
|
||||
break;
|
||||
case "EADDRINUSE":
|
||||
console.error(`${bind} is already in use`);
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function onListening() {
|
||||
const addr = server.address();
|
||||
if (!addr) return;
|
||||
|
||||
const bind = typeof addr === "string"
|
||||
? `pipe ${addr}`
|
||||
: `port ${addr.port}`;
|
||||
|
||||
startCronJobs();
|
||||
// TODO - uncomment initRedis()
|
||||
// void initRedis();
|
||||
FileConstants.init();
|
||||
void DbTaskStatusChangeListener.connect();
|
||||
|
||||
console.info(`Listening on ${bind}`);
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
DbTaskStatusChangeListener.disconnect();
|
||||
}
|
||||
|
||||
server.on("error", onError);
|
||||
server.on("close", onClose);
|
||||
server.on("listening", onListening);
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
server.listen(port);
|
||||
9
worklenz-backend/src/config/db-config.ts
Normal file
9
worklenz-backend/src/config/db-config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
host: process.env.DB_HOST,
|
||||
port: +(process.env.DB_PORT as string),
|
||||
max: +(process.env.DB_MAX_CLIENTS as string),
|
||||
idleTimeoutMillis: 30000,
|
||||
};
|
||||
15
worklenz-backend/src/config/db.ts
Normal file
15
worklenz-backend/src/config/db.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import pgModule, {QueryResult} from "pg";
|
||||
import dbConfig from "./db-config";
|
||||
|
||||
const pg = (process.env.USE_PG_NATIVE === "true" && pgModule.native) ? pgModule.native : pgModule;
|
||||
const pool = new pg.Pool(dbConfig);
|
||||
|
||||
pool.on("error", (err: Error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("pg idle client error", err, err.message, err.stack);
|
||||
});
|
||||
|
||||
export default {
|
||||
pool,
|
||||
query: (text: string, params?: unknown[]) => pool.query(text, params) as Promise<QueryResult<any>>,
|
||||
};
|
||||
0
worklenz-backend/src/controllers/.gitkeep
Normal file
0
worklenz-backend/src/controllers/.gitkeep
Normal file
@@ -0,0 +1,15 @@
|
||||
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";
|
||||
|
||||
export default class AccessControlsController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getRoles(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name, default_role, admin_role FROM roles WHERE team_id = $1 AND owner IS FALSE ORDER BY name;`;
|
||||
const result = await db.query(q, [req.user?.team_id || null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
34
worklenz-backend/src/controllers/activity-logs-controller.ts
Normal file
34
worklenz-backend/src/controllers/activity-logs-controller.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import moment from "moment";
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {formatDuration, formatLogText, getColor} from "../shared/utils";
|
||||
|
||||
export default class ActivitylogsController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {id} = req.params;
|
||||
const q = `SELECT get_activity_logs_by_task($1) AS activity_logs;`;
|
||||
const result = await db.query(q, [id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
for (const log of data.activity_logs.logs) {
|
||||
if (log.attribute_type === "estimation") {
|
||||
log.previous = formatDuration(moment.duration(log.previous, "minutes"));
|
||||
log.current = formatDuration(moment.duration(log.current, "minutes"));
|
||||
}
|
||||
if (log.assigned_user) log.assigned_user.color_code = getColor(log.assigned_user.name);
|
||||
log.done_by.color_code = getColor(log.done_by.name);
|
||||
log.log_text = await formatLogText(log);
|
||||
log.attribute_type = log.attribute_type?.replace(/_/g, " ");
|
||||
}
|
||||
data.activity_logs.color_code = getColor(data.activity_logs.name);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.activity_logs));
|
||||
}
|
||||
}
|
||||
309
worklenz-backend/src/controllers/admin-center-controller.ts
Normal file
309
worklenz-backend/src/controllers/admin-center-controller.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {getColor} from "../shared/utils";
|
||||
import {calculateStorage} from "../shared/s3";
|
||||
import {NotificationsService} from "../services/notifications/notifications.service";
|
||||
import {SocketEvents} from "../socket.io/events";
|
||||
import {IO} from "../shared/io";
|
||||
|
||||
export default class AdminCenterController extends WorklenzControllerBase {
|
||||
|
||||
public static async checkIfUserActiveInOtherTeams(owner_id: string, email: string) {
|
||||
if (!owner_id) throw new Error("Owner not found.");
|
||||
|
||||
const q = `SELECT EXISTS(SELECT tmi.team_member_id
|
||||
FROM team_member_info_view AS tmi
|
||||
JOIN teams AS t ON tmi.team_id = t.id
|
||||
JOIN team_members AS tm ON tmi.team_member_id = tm.id
|
||||
WHERE tmi.email = $1::TEXT
|
||||
AND t.user_id = $2::UUID AND tm.active = true);`;
|
||||
const result = await db.query(q, [email, owner_id]);
|
||||
|
||||
const [data] = result.rows;
|
||||
return data.exists;
|
||||
}
|
||||
|
||||
// organization
|
||||
@HandleExceptions()
|
||||
public static async getOrganizationDetails(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
// const q = `SELECT organization_name AS name,
|
||||
// contact_number,
|
||||
// contact_number_secondary,
|
||||
// (SELECT email FROM users WHERE id = users_data.user_id),
|
||||
// (SELECT name FROM users WHERE id = users_data.user_id) AS owner_name
|
||||
// FROM users_data
|
||||
// WHERE user_id = $1;`;
|
||||
const q = `SELECT organization_name AS name,
|
||||
contact_number,
|
||||
contact_number_secondary,
|
||||
(SELECT email FROM users WHERE id = organizations.user_id),
|
||||
(SELECT name FROM users WHERE id = organizations.user_id) AS owner_name
|
||||
FROM organizations
|
||||
WHERE user_id = $1;`;
|
||||
const result = await db.query(q, [req.user?.owner_id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getOrganizationAdmins(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT u.name, email, owner AS is_owner
|
||||
FROM users u
|
||||
LEFT JOIN team_members tm ON u.id = tm.user_id
|
||||
LEFT JOIN roles r ON tm.role_id = r.id
|
||||
WHERE tm.team_id IN (SELECT id FROM teams WHERE teams.user_id = $1)
|
||||
AND (admin_role IS TRUE OR owner IS TRUE)
|
||||
GROUP BY u.name, email, owner
|
||||
ORDER BY owner DESC, u.name;`;
|
||||
const result = await db.query(q, [req.user?.owner_id]);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getOrganizationUsers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { searchQuery, size, offset } = this.toPaginationOptions(req.query, ["outer_tmiv.name", "outer_tmiv.email"]);
|
||||
|
||||
const q = `SELECT ROW_TO_JSON(rec) AS users
|
||||
FROM (SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT email,
|
||||
STRING_AGG(DISTINCT CAST(user_id AS VARCHAR), ', ') AS user_id,
|
||||
STRING_AGG(DISTINCT name, ', ') AS name,
|
||||
STRING_AGG(DISTINCT avatar_url, ', ') AS avatar_url,
|
||||
(SELECT twl.created_at
|
||||
FROM task_work_log twl
|
||||
WHERE twl.user_id IN (SELECT tmiv.user_id
|
||||
FROM team_member_info_view tmiv
|
||||
WHERE tmiv.email = outer_tmiv.email)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1) AS last_logged
|
||||
FROM team_member_info_view outer_tmiv
|
||||
WHERE outer_tmiv.team_id IN (SELECT id
|
||||
FROM teams
|
||||
WHERE teams.user_id = $1) ${searchQuery}
|
||||
GROUP BY email
|
||||
ORDER BY email LIMIT $2 OFFSET $3) t) AS data
|
||||
FROM (SELECT DISTINCT email
|
||||
FROM team_member_info_view outer_tmiv
|
||||
WHERE outer_tmiv.team_id IN
|
||||
(SELECT id
|
||||
FROM teams
|
||||
WHERE teams.user_id = $1) ${searchQuery}) AS total) rec;`;
|
||||
const result = await db.query(q, [req.user?.owner_id, size, offset]);
|
||||
const [data] = result.rows;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.users));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateOrganizationName(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {name} = req.body;
|
||||
// const q = `UPDATE users_data
|
||||
// SET organization_name = $1
|
||||
// WHERE user_id = $2;`;
|
||||
const q = `UPDATE organizations
|
||||
SET organization_name = $1
|
||||
WHERE user_id = $2;`;
|
||||
const result = await db.query(q, [name, req.user?.owner_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateOwnerContactNumber(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {contact_number} = req.body;
|
||||
const q = `UPDATE organizations
|
||||
SET contact_number = $1
|
||||
WHERE user_id = $2;`;
|
||||
const result = await db.query(q, [contact_number, req.user?.owner_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = ``;
|
||||
const result = await db.query(q, []);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getOrganizationTeams(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {searchQuery, size, offset} = this.toPaginationOptions(req.query, ["name"]);
|
||||
|
||||
let size_changed = size;
|
||||
|
||||
if (offset == 0) size_changed = size_changed - 1;
|
||||
|
||||
const currentTeamClosure = offset == 0 ? `,
|
||||
(SELECT COALESCE(ROW_TO_JSON(c), '{}'::JSON)
|
||||
FROM (SELECT id,
|
||||
name,
|
||||
created_at,
|
||||
(SELECT count(*) FROM team_members WHERE team_id = teams.id) as members_count,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT CASE
|
||||
WHEN u.name IS NOT NULL THEN u.name
|
||||
ELSE (SELECT name
|
||||
FROM email_invitations
|
||||
WHERE team_member_id = team_members.id) END,
|
||||
avatar_url
|
||||
FROM team_members
|
||||
LEFT JOIN users u on team_members.user_id = u.id
|
||||
WHERE team_id = teams.id) rec) AS team_members
|
||||
FROM teams
|
||||
WHERE user_id = $1 AND teams.id = $4) c) AS current_team_data` : ``;
|
||||
|
||||
const q = `SELECT ROW_TO_JSON(rec) AS teams
|
||||
FROM (SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT id,
|
||||
name,
|
||||
created_at,
|
||||
(SELECT count(*) FROM team_members WHERE team_id = teams.id) as members_count,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT CASE
|
||||
WHEN u.name IS NOT NULL THEN u.name
|
||||
ELSE (SELECT name
|
||||
FROM email_invitations
|
||||
WHERE team_member_id = team_members.id) END,
|
||||
avatar_url
|
||||
FROM team_members
|
||||
LEFT JOIN users u on team_members.user_id = u.id
|
||||
WHERE team_id = teams.id) rec) AS team_members
|
||||
FROM teams
|
||||
WHERE user_id = $1 AND NOT teams.id = $4 ${searchQuery}
|
||||
ORDER BY name, created_at
|
||||
LIMIT $2 OFFSET $3) t) AS data
|
||||
${currentTeamClosure}
|
||||
FROM teams
|
||||
WHERE user_id = $1 ${searchQuery}) rec;`;
|
||||
const result = await db.query(q, [req.user?.owner_id, size_changed, offset, req.user?.team_id]);
|
||||
|
||||
const [obj] = result.rows;
|
||||
|
||||
for (const team of obj.teams?.data || []) {
|
||||
team.names = this.createTagList(team?.team_members);
|
||||
team.names.map((a: any) => a.color_code = getColor(a.name));
|
||||
}
|
||||
|
||||
if (obj.teams.current_team_data) {
|
||||
obj.teams.current_team_data.names = this.createTagList(obj.teams.current_team_data?.team_members);
|
||||
obj.teams.current_team_data.names.map((a: any) => a.color_code = getColor(a.name));
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, obj.teams || {}));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTeamDetails(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {id} = req.params;
|
||||
|
||||
const q = `SELECT id,
|
||||
name,
|
||||
created_at,
|
||||
(SELECT count(*) FROM team_members WHERE team_id = teams.id) as members_count,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT tm.id,
|
||||
tm.user_id,
|
||||
(SELECT name
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = tm.id),
|
||||
(SELECT team_member_info_view.email
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = tm.id),
|
||||
(SELECT team_member_info_view.avatar_url
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = tm.id),
|
||||
role_id,
|
||||
r.name AS role_name
|
||||
FROM team_members tm
|
||||
LEFT JOIN users u on tm.user_id = u.id
|
||||
LEFT JOIN roles r on tm.role_id = r.id
|
||||
WHERE tm.team_id = teams.id
|
||||
ORDER BY r.name = 'Owner' DESC, u.name) rec) AS team_members
|
||||
FROM teams
|
||||
WHERE id = $1;`;
|
||||
const result = await db.query(q, [id]);
|
||||
|
||||
const [obj] = result.rows;
|
||||
|
||||
obj.names = this.createTagList(obj?.team_members);
|
||||
obj.names.map((a: any) => a.color_code = getColor(a.name));
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, obj || {}));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {id} = req.params;
|
||||
const {name, teamMembers} = req.body;
|
||||
|
||||
const updateNameQuery = `UPDATE teams
|
||||
SET name = $1
|
||||
WHERE id = $2;`;
|
||||
await db.query(updateNameQuery, [name, id]);
|
||||
|
||||
if (teamMembers.length) {
|
||||
teamMembers.forEach(async (element: { role_name: string; user_id: string; }) => {
|
||||
const q = `UPDATE team_members
|
||||
SET role_id = (SELECT id FROM roles WHERE roles.team_id = $1 AND name = $2)
|
||||
WHERE user_id = $3
|
||||
AND team_id = $1;`;
|
||||
await db.query(q, [id, element.role_name, element.user_id]);
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, [], "Team updated successfully"));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {id} = req.params;
|
||||
|
||||
if (id == req.user?.team_id) {
|
||||
return res.status(200).send(new ServerResponse(true, [], "Please switch to another team before attempting deletion.")
|
||||
.withTitle("Unable to remove the presently active team!"));
|
||||
}
|
||||
|
||||
const q = `DELETE FROM teams WHERE id = $1;`;
|
||||
const result = await db.query(q, [id]);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {id} = req.params;
|
||||
const {teamId} = req.body;
|
||||
|
||||
if (!id || !teamId) return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
|
||||
|
||||
|
||||
const q = `SELECT remove_team_member($1, $2, $3) AS member;`;
|
||||
const result = await db.query(q, [id, req.user?.id, teamId]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const message = `You have been removed from <b>${req.user?.team_name}</b> by <b>${req.user?.name}</b>`;
|
||||
|
||||
NotificationsService.sendNotification({
|
||||
receiver_socket_id: data.socket_id,
|
||||
message,
|
||||
team: data.team,
|
||||
team_id: id
|
||||
});
|
||||
|
||||
IO.emitByUserId(data.member.id, req.user?.id || null, SocketEvents.TEAM_MEMBER_REMOVED, {
|
||||
teamId: id,
|
||||
message
|
||||
});
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
163
worklenz-backend/src/controllers/attachment-controller.ts
Normal file
163
worklenz-backend/src/controllers/attachment-controller.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import { humanFileSize, log_error, smallId } from "../shared/utils";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import {
|
||||
createPresignedUrlWithClient,
|
||||
deleteObject,
|
||||
getAvatarKey,
|
||||
getKey,
|
||||
getRootDir,
|
||||
uploadBase64,
|
||||
uploadBuffer
|
||||
} from "../shared/s3";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
const {S3_URL} = process.env;
|
||||
|
||||
if (!S3_URL) {
|
||||
log_error("Invalid S3_URL. Please check .env file.");
|
||||
}
|
||||
|
||||
export default class AttachmentController extends WorklenzControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async createTaskAttachment(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { file, file_name, task_id, project_id, size, type } = req.body;
|
||||
|
||||
const q = `
|
||||
INSERT INTO task_attachments (name, task_id, team_id, project_id, uploaded_by, size, type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, name, size, type, created_at, CONCAT($8::TEXT, '/', team_id, '/', project_id, '/', id, '.', type) AS url;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [
|
||||
file_name,
|
||||
task_id,
|
||||
req.user?.team_id,
|
||||
project_id,
|
||||
req.user?.id,
|
||||
size,
|
||||
type,
|
||||
`${S3_URL}/${getRootDir()}`
|
||||
]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const s3Url = await uploadBase64(file, getKey(req.user?.team_id as string, project_id, data.id, data.type));
|
||||
|
||||
if (!data?.id || !s3Url)
|
||||
return res.status(200).send(new ServerResponse(false, null, "Attachment upload failed"));
|
||||
|
||||
data.size = humanFileSize(data.size);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async createAvatarAttachment(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { type, buffer } = req.body;
|
||||
|
||||
const s3Url = await uploadBuffer(buffer as Buffer, type, getAvatarKey(req.user?.id as string, type));
|
||||
|
||||
if (!s3Url)
|
||||
return res.status(200).send(new ServerResponse(false, null, "Avatar upload failed"));
|
||||
|
||||
const q = "UPDATE users SET avatar_url = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING avatar_url;";
|
||||
const result = await db.query(q, [req.user?.id, `${s3Url}?v=${smallId(4)}`]);
|
||||
const [data] = result.rows;
|
||||
if (!data)
|
||||
return res.status(200).send(new ServerResponse(false, null, "Avatar upload failed"));
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, { url: data.avatar_url }, "Avatar updated."));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT id,
|
||||
name,
|
||||
size,
|
||||
CONCAT($2::TEXT, '/', team_id, '/', project_id, '/', id, '.', type) AS url,
|
||||
type,
|
||||
created_at
|
||||
FROM task_attachments
|
||||
WHERE task_id = $1;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, `${S3_URL}/${getRootDir()}`]);
|
||||
|
||||
for (const item of result.rows)
|
||||
item.size = humanFileSize(item.size);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { size, offset } = this.toPaginationOptions(req.query, "name");
|
||||
|
||||
const q = `
|
||||
SELECT ROW_TO_JSON(rec) AS attachments
|
||||
FROM (SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT task_attachments.id,
|
||||
task_attachments.name,
|
||||
CONCAT((SELECT key FROM projects WHERE id = task_attachments.project_id), '-',
|
||||
(SELECT task_no FROM tasks WHERE id = task_attachments.task_id)) AS task_key,
|
||||
size,
|
||||
CONCAT($2::TEXT, '/', task_attachments.team_id, '/', task_attachments.project_id, '/',task_attachments.id,'.',type) AS url,
|
||||
task_attachments.type,
|
||||
task_attachments.created_at,
|
||||
t.name AS task_name,
|
||||
(SELECT name FROM users WHERE id = task_attachments.uploaded_by) AS uploader_name
|
||||
FROM task_attachments
|
||||
LEFT JOIN tasks t ON task_attachments.task_id = t.id
|
||||
WHERE task_attachments.project_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $3 OFFSET $4)t) AS data
|
||||
FROM task_attachments
|
||||
LEFT JOIN tasks t ON task_attachments.task_id = t.id
|
||||
WHERE task_attachments.project_id = $1) rec;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, `${S3_URL}/${getRootDir()}`, size, offset]);
|
||||
const [data] = result.rows;
|
||||
|
||||
for (const item of data?.attachments.data || [])
|
||||
item.size = humanFileSize(item.size);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data?.attachments || this.paginatedDatasetDefaultStruct));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `DELETE
|
||||
FROM task_attachments
|
||||
WHERE id = $1
|
||||
RETURNING CONCAT($2::TEXT, '/', team_id, '/', project_id, '/', id, '.', type) AS key;`;
|
||||
const result = await db.query(q, [req.params.id, getRootDir()]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (data?.key)
|
||||
void deleteObject(data.key);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async download(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT CONCAT($2::TEXT, '/', team_id, '/', project_id, '/', id, '.', type) AS key
|
||||
FROM task_attachments
|
||||
WHERE id = $1;`;
|
||||
const result = await db.query(q, [req.query.id, getRootDir()]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (data?.key) {
|
||||
const url = await createPresignedUrlWithClient(data.key, req.query.file as string);
|
||||
return res.status(200).send(new ServerResponse(true, url));
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, null));
|
||||
}
|
||||
}
|
||||
141
worklenz-backend/src/controllers/auth-controller.ts
Normal file
141
worklenz-backend/src/controllers/auth-controller.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
import {sendResetEmail, sendResetSuccessEmail} from "../shared/email-templates";
|
||||
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import {AuthResponse} from "../models/auth-response";
|
||||
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
import db from "../config/db";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {PasswordStrengthChecker} from "../shared/password-strength-check";
|
||||
import FileConstants from "../shared/file-constants";
|
||||
|
||||
export default class AuthController extends WorklenzControllerBase {
|
||||
/** This just send ok response to the client when the request came here through the sign-up-validator */
|
||||
public static async status_check(_req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
return res.status(200).send(new ServerResponse(true, null));
|
||||
}
|
||||
|
||||
public static async checkPasswordStrength(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
const result = PasswordStrengthChecker.validate(req.query.password as string);
|
||||
return res.status(200).send(new ServerResponse(true, result));
|
||||
}
|
||||
|
||||
public static verify(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
// Flash messages sent from passport-local-signup.ts and passport-local-login.ts
|
||||
const errors = req.flash()["error"] || [];
|
||||
const messages = req.flash()["success"] || [];
|
||||
// If there are multiple messages, we will send one at a time.
|
||||
const auth_error = errors.length > 0 ? errors[0] : null;
|
||||
const message = messages.length > 0 ? messages[0] : null;
|
||||
|
||||
const midTitle = req.query.strategy === "login" ? "Login Failed!" : "Signup Failed!";
|
||||
const title = req.query.strategy ? midTitle : null;
|
||||
|
||||
if (req.user)
|
||||
req.user.build_v = FileConstants.getRelease();
|
||||
|
||||
return res.status(200).send(new AuthResponse(title, req.isAuthenticated(), req.user || null, auth_error, message));
|
||||
}
|
||||
|
||||
public static logout(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
req.logout(() => true);
|
||||
req.session.destroy(() => {
|
||||
res.redirect("/");
|
||||
});
|
||||
}
|
||||
|
||||
private static async destroyOtherSessions(userId: string, sessionId: string) {
|
||||
try {
|
||||
const q = `DELETE FROM pg_sessions WHERE (sess ->> 'passport')::JSON ->> 'user'::TEXT = $1 AND sid != $2;`;
|
||||
await db.query(q, [userId, sessionId]);
|
||||
} catch (error) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async changePassword(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
|
||||
const currentPassword = req.body.password;
|
||||
const newPassword = req.body.new_password;
|
||||
|
||||
const q = `SELECT id, email, google_id, password FROM users WHERE id = $1;`;
|
||||
const result = await db.query(q, [req.user?.id || null]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (data) {
|
||||
// Compare the password
|
||||
if (bcrypt.compareSync(currentPassword, data.password)) {
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
const encryptedPassword = bcrypt.hashSync(newPassword, salt);
|
||||
|
||||
const updatePasswordQ = `UPDATE users SET password = $1 WHERE id = $2;`;
|
||||
await db.query(updatePasswordQ, [encryptedPassword, req.user?.id || null]);
|
||||
|
||||
if (req.user?.id)
|
||||
AuthController.destroyOtherSessions(req.user.id, req.sessionID);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, null, "Password updated successfully!"));
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(false, null, "Old password does not match!"));
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions({logWithError: "body"})
|
||||
public static async reset_password(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
const {email} = req.body;
|
||||
|
||||
const q = `SELECT id, email, google_id, password FROM users WHERE email = $1;`;
|
||||
const result = await db.query(q, [email || null]);
|
||||
|
||||
if (!result.rowCount)
|
||||
return res.status(200).send(new ServerResponse(false, null, "Account does not exists!"));
|
||||
|
||||
const [data] = result.rows;
|
||||
|
||||
if (data?.google_id) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Password reset failed!"));
|
||||
}
|
||||
|
||||
if (data?.password) {
|
||||
const userIdBase64 = Buffer.from(data.id, "utf8").toString("base64");
|
||||
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
const hashedUserData = bcrypt.hashSync(data.id + data.email + data.password, salt);
|
||||
const hashedString = hashedUserData.toString().replace(/\//g, "-");
|
||||
|
||||
sendResetEmail(email, userIdBase64, hashedString);
|
||||
return res.status(200).send(new ServerResponse(true, null, "Password reset email has been sent to your email. Please check your email."));
|
||||
}
|
||||
return res.status(200).send(new ServerResponse(false, null, "Email not found!"));
|
||||
}
|
||||
|
||||
@HandleExceptions({logWithError: "body"})
|
||||
public static async verify_reset_email(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
const {user, hash, password} = req.body;
|
||||
const hashedString = hash.replace(/\-/g, "/");
|
||||
|
||||
const userId = Buffer.from(user as string, "base64").toString("ascii");
|
||||
|
||||
const q = `SELECT id, email, google_id, password FROM users WHERE id = $1;`;
|
||||
const result = await db.query(q, [userId || null]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
|
||||
if (bcrypt.compareSync(data.id + data.email + data.password, hashedString)) {
|
||||
const encryptedPassword = bcrypt.hashSync(password, salt);
|
||||
const updatePasswordQ = `UPDATE users SET password = $1 WHERE id = $2;`;
|
||||
await db.query(updatePasswordQ, [encryptedPassword, userId || null]);
|
||||
|
||||
sendResetSuccessEmail(data.email);
|
||||
return res.status(200).send(new ServerResponse(true, null, "Password updated successfully"));
|
||||
}
|
||||
return res.status(200).send(new ServerResponse(false, null, "Invalid Request. Please try again."));
|
||||
}
|
||||
}
|
||||
58
worklenz-backend/src/controllers/aws-ses-controller.ts
Normal file
58
worklenz-backend/src/controllers/aws-ses-controller.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {ISESBouncedMessage} from "../interfaces/aws-bounced-email-response";
|
||||
import db from "../config/db";
|
||||
import {ISESComplaintMessage} from "../interfaces/aws-complaint-email-response";
|
||||
|
||||
export default class AwsSesController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async handleBounceResponse(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const message = JSON.parse(req.body.Message) as ISESBouncedMessage;
|
||||
|
||||
if (message.notificationType === "Bounce" && message.bounce.bounceType === "Permanent") {
|
||||
const bouncedEmails = Array.from(new Set(message.bounce.bouncedRecipients.map(r => r.emailAddress)));
|
||||
|
||||
for (const email of bouncedEmails) {
|
||||
const q = `
|
||||
INSERT INTO bounced_emails (email)
|
||||
VALUES ($1)
|
||||
ON CONFLICT (email) DO UPDATE SET updated_at = CURRENT_TIMESTAMP;
|
||||
`;
|
||||
await db.query(q, [email]);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, null));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async handleComplaintResponse(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const message = JSON.parse(req.body.Message) as ISESComplaintMessage;
|
||||
|
||||
if (message.notificationType === "Complaint") {
|
||||
const spamEmails = Array.from(new Set(message.complaint.complainedRecipients.map(r => r.emailAddress)));
|
||||
|
||||
for (const email of spamEmails) {
|
||||
const q = `
|
||||
INSERT INTO spam_emails (email)
|
||||
VALUES ($1)
|
||||
ON CONFLICT (email) DO UPDATE SET updated_at = CURRENT_TIMESTAMP;
|
||||
`;
|
||||
await db.query(q, [email]);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, null));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async handleReplies(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
console.log("\n");
|
||||
console.log(JSON.stringify(req.body));
|
||||
console.log("\n");
|
||||
return res.status(200).send(new ServerResponse(true, null));
|
||||
}
|
||||
}
|
||||
81
worklenz-backend/src/controllers/clients-controller.ts
Normal file
81
worklenz-backend/src/controllers/clients-controller.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {isValidateEmail} from "../shared/utils";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import {sendNewSubscriberNotification} from "../shared/email-templates";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class ClientsController extends WorklenzControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `INSERT INTO clients (name, team_id) VALUES ($1, $2);`;
|
||||
const result = await db.query(q, [req.body.name, req.user?.team_id || null]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {searchQuery, sortField, sortOrder, size, offset} = this.toPaginationOptions(req.query, "name");
|
||||
|
||||
const q = `
|
||||
SELECT ROW_TO_JSON(rec) AS clients
|
||||
FROM (SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT id,
|
||||
name,
|
||||
(SELECT COUNT(*) FROM projects WHERE client_id = clients.id) AS projects_count
|
||||
FROM clients
|
||||
WHERE team_id = $1 ${searchQuery}
|
||||
ORDER BY ${sortField} ${sortOrder}
|
||||
LIMIT $2 OFFSET $3) t) AS data
|
||||
FROM clients
|
||||
WHERE team_id = $1 ${searchQuery}) rec;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.team_id || null, size, offset]);
|
||||
const [data] = result.rows;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.clients || this.paginatedDatasetDefaultStruct));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name FROM clients WHERE id = $1 AND team_id = $2`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `UPDATE clients SET name = $3 WHERE id = $1 AND team_id = $2; `;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id || null, req.body.name]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `DELETE FROM clients WHERE id = $1 AND team_id = $2;`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async addSubscriber(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {email} = req.body;
|
||||
if (!this.isValidHost(req.hostname))
|
||||
return res.status(200).send(new ServerResponse(false, null, "Invalid hostname"));
|
||||
|
||||
if (!isValidateEmail(email))
|
||||
return res.status(200).send(new ServerResponse(false, null, "Invalid email address"));
|
||||
|
||||
sendNewSubscriberNotification(email);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, null, "Thank you for subscribing. We'll update you once WorkLenz is live!"));
|
||||
}
|
||||
|
||||
}
|
||||
97
worklenz-backend/src/controllers/gantt-controller.ts
Normal file
97
worklenz-backend/src/controllers/gantt-controller.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import { getColor } from "../shared/utils";
|
||||
import moment from "moment";
|
||||
|
||||
export default class GanttController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getPhaseLabel(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT phase_label
|
||||
FROM projects
|
||||
WHERE id = $1;`;
|
||||
const result = await db.query(q, [req.query.project_id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id AS "TaskID",
|
||||
name AS "TaskName",
|
||||
start_date AS "StartDate",
|
||||
end_date AS "EndDate",
|
||||
(SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = tasks.status_id)),
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT id AS "TaskID",
|
||||
name AS "TaskName",
|
||||
start_date AS "StartDate",
|
||||
end_date AS "EndDate",
|
||||
(SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = tasks.status_id))
|
||||
FROM tasks t
|
||||
WHERE t.parent_task_id = tasks.id) rec) AS subtasks
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = $1
|
||||
AND parent_task_id IS NULL
|
||||
ORDER BY roadmap_sort_order, created_at DESC;`;
|
||||
const result = await db.query(q, [req.query.project_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getPhasesByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT name AS label,
|
||||
(SELECT MIN(start_date)
|
||||
FROM tasks
|
||||
WHERE id IN (SELECT task_id FROM task_phase WHERE phase_id = project_phases.id)) as day
|
||||
FROM project_phases
|
||||
WHERE project_id = $1;`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
for (const phase of result.rows) {
|
||||
phase.day = new Date(phase.day);
|
||||
}
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getWorkload(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT pm.id AS "TaskID",
|
||||
tmiv.team_member_id,
|
||||
name AS "TaskName",
|
||||
avatar_url,
|
||||
email,
|
||||
TRUE as project_member,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT id AS "TaskID",
|
||||
name AS "TaskName",
|
||||
start_date AS "StartDate",
|
||||
end_date AS "EndDate"
|
||||
FROM tasks
|
||||
INNER JOIN tasks_assignees ta ON tasks.id = ta.task_id
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = pm.project_id
|
||||
AND ta.team_member_id = tmiv.team_member_id
|
||||
ORDER BY roadmap_sort_order, start_date DESC) rec) AS subtasks
|
||||
FROM project_members pm
|
||||
INNER JOIN team_member_info_view tmiv ON pm.team_member_id = tmiv.team_member_id
|
||||
WHERE project_id = $1
|
||||
ORDER BY tmiv.name;`;
|
||||
const result = await db.query(q, [req.query.project_id]);
|
||||
|
||||
for (const member of result.rows) {
|
||||
member.color_code = getColor(member.TaskName);
|
||||
}
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
422
worklenz-backend/src/controllers/home-page-controller.ts
Normal file
422
worklenz-backend/src/controllers/home-page-controller.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
import moment from "moment-timezone";
|
||||
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";
|
||||
import momentTime from "moment-timezone";
|
||||
|
||||
interface ITask {
|
||||
id: string,
|
||||
name: string,
|
||||
project_id: string,
|
||||
parent_task_id: string | null,
|
||||
is_sub_task: boolean,
|
||||
parent_task_name: string | null,
|
||||
status_id: string,
|
||||
start_date: string | null,
|
||||
end_date: string | null,
|
||||
created_at: string | null,
|
||||
team_id: string,
|
||||
project_name: string,
|
||||
project_color: string | null,
|
||||
status: string,
|
||||
status_color: string | null,
|
||||
is_task: boolean,
|
||||
done: boolean,
|
||||
updated_at: string | null,
|
||||
project_statuses: [{
|
||||
id: string,
|
||||
name: string | null,
|
||||
color_code: string | null,
|
||||
}]
|
||||
}
|
||||
|
||||
export default class HomePageController extends WorklenzControllerBase {
|
||||
|
||||
private static readonly GROUP_BY_ASSIGNED_TO_ME = "0";
|
||||
private static readonly GROUP_BY_ASSIGN_BY_ME = "1";
|
||||
private static readonly ALL_TAB = "All";
|
||||
private static readonly TODAY_TAB = "Today";
|
||||
private static readonly UPCOMING_TAB = "Upcoming";
|
||||
private static readonly OVERDUE_TAB = "Overdue";
|
||||
private static readonly NO_DUE_DATE_TAB = "NoDueDate";
|
||||
|
||||
private static isValidGroup(groupBy: string) {
|
||||
return groupBy === this.GROUP_BY_ASSIGNED_TO_ME
|
||||
|| groupBy === this.GROUP_BY_ASSIGN_BY_ME;
|
||||
}
|
||||
|
||||
private static isValidView(currentView: string) {
|
||||
return currentView === this.ALL_TAB
|
||||
|| currentView === this.TODAY_TAB
|
||||
|| currentView === this.UPCOMING_TAB
|
||||
|| currentView === this.OVERDUE_TAB
|
||||
|| currentView === this.NO_DUE_DATE_TAB;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async createPersonalTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `INSERT INTO personal_todo_list (name, color_code, user_id, index)
|
||||
VALUES ($1, $2, $3, ((SELECT index FROM personal_todo_list ORDER BY index DESC LIMIT 1) + 1))
|
||||
RETURNING id, name`;
|
||||
const result = await db.query(q, [req.body.name, req.body.color_code, req.user?.id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
private static getTasksByGroupClosure(groupBy: string) {
|
||||
switch (groupBy) {
|
||||
case this.GROUP_BY_ASSIGN_BY_ME:
|
||||
return `AND t.id IN (
|
||||
SELECT task_id
|
||||
FROM tasks_assignees
|
||||
WHERE assigned_by = $2 AND team_id = $1)`;
|
||||
|
||||
case this.GROUP_BY_ASSIGNED_TO_ME:
|
||||
default:
|
||||
return `AND t.id IN (
|
||||
SELECT task_id
|
||||
FROM tasks_assignees
|
||||
WHERE team_member_id = (SELECT id FROM team_members WHERE user_id = $2 AND team_id = $1))`;
|
||||
}
|
||||
}
|
||||
|
||||
private static getTasksByTabClosure(text: string) {
|
||||
switch (text) {
|
||||
case this.TODAY_TAB:
|
||||
return `AND t.end_date::DATE = CURRENT_DATE::DATE`;
|
||||
case this.UPCOMING_TAB:
|
||||
return `AND t.end_date::DATE > CURRENT_DATE::DATE`;
|
||||
case this.OVERDUE_TAB:
|
||||
return `AND t.end_date::DATE < CURRENT_DATE::DATE`;
|
||||
case this.NO_DUE_DATE_TAB:
|
||||
return `AND t.end_date IS NULL`;
|
||||
case this.ALL_TAB:
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static async getTasksResult(groupByClosure: string, currentTabClosure: string, teamId: string, userId: string) {
|
||||
const q = `
|
||||
SELECT t.id,
|
||||
t.name,
|
||||
t.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,
|
||||
t.status_id,
|
||||
t.start_date,
|
||||
t.end_date,
|
||||
t.created_at,
|
||||
p.team_id,
|
||||
p.name AS project_name,
|
||||
p.color_code AS project_color,
|
||||
(SELECT id FROM task_statuses WHERE id = t.status_id) AS status,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
|
||||
TRUE AS is_task,
|
||||
FALSE AS done,
|
||||
t.updated_at,
|
||||
(SELECT ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r)))
|
||||
FROM (SELECT task_statuses.id AS id,
|
||||
task_statuses.name AS name,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = task_statuses.category_id)
|
||||
FROM task_statuses
|
||||
WHERE task_statuses.project_id = t.project_id) r) AS project_statuses
|
||||
FROM tasks t
|
||||
JOIN projects p ON t.project_id = p.id
|
||||
WHERE t.archived IS FALSE
|
||||
AND t.status_id NOT IN (SELECT id
|
||||
FROM task_statuses
|
||||
WHERE category_id NOT IN (SELECT id
|
||||
FROM sys_task_status_categories
|
||||
WHERE is_done IS FALSE))
|
||||
${groupByClosure}
|
||||
ORDER BY t.end_date ASC`;
|
||||
|
||||
const result = await db.query(q, [teamId, userId]);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
private static async getCountsResult(groupByClosure: string, teamId: string, userId: string) {
|
||||
const q = `SELECT COUNT(*) AS total,
|
||||
COUNT(CASE WHEN t.end_date::DATE = CURRENT_DATE::DATE THEN 1 END) AS today,
|
||||
COUNT(CASE WHEN t.end_date::DATE > CURRENT_DATE::DATE THEN 1 END) AS upcoming,
|
||||
COUNT(CASE WHEN t.end_date::DATE < CURRENT_DATE::DATE THEN 1 END) AS overdue,
|
||||
COUNT(CASE WHEN t.end_date::DATE IS NULL THEN 1 END) AS no_due_date
|
||||
FROM tasks t
|
||||
JOIN projects p ON t.project_id = p.id
|
||||
WHERE t.archived IS FALSE
|
||||
AND t.status_id NOT IN (SELECT id
|
||||
FROM task_statuses
|
||||
WHERE category_id NOT IN (SELECT id
|
||||
FROM sys_task_status_categories
|
||||
WHERE is_done IS FALSE))
|
||||
${groupByClosure}`;
|
||||
|
||||
const result = await db.query(q, [teamId, userId]);
|
||||
const [row] = result.rows;
|
||||
return row;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const teamId = req.user?.team_id;
|
||||
const userId = req.user?.id;
|
||||
const timeZone = req.query.time_zone as string;
|
||||
const today = new Date();
|
||||
|
||||
const currentGroup = this.isValidGroup(req.query.group_by as string) ? req.query.group_by : this.GROUP_BY_ASSIGNED_TO_ME;
|
||||
const currentTab = this.isValidView(req.query.current_tab as string) ? req.query.current_tab : this.ALL_TAB;
|
||||
|
||||
const groupByClosure = this.getTasksByGroupClosure(currentGroup as string);
|
||||
let currentTabClosure = this.getTasksByTabClosure(currentTab as string);
|
||||
|
||||
const isCalendarView = req.query.is_calendar_view;
|
||||
|
||||
let result = await this.getTasksResult(groupByClosure, currentTabClosure, teamId as string, userId as string);
|
||||
|
||||
const counts = await this.getCountsByGroup(result, timeZone, today);
|
||||
|
||||
if (isCalendarView == "true") {
|
||||
currentTabClosure = `AND t.end_date::DATE = '${req.query.selected_date}'`;
|
||||
result = await this.groupBySingleDate(result, timeZone, req.query.selected_date as string);
|
||||
} else {
|
||||
result = await this.groupByDate(currentTab as string,result, timeZone, today);
|
||||
}
|
||||
|
||||
// const counts = await this.getCountsResult(groupByClosure, teamId as string, userId as string);
|
||||
|
||||
const data = {
|
||||
tasks: result,
|
||||
total: counts.total,
|
||||
today: counts.today,
|
||||
upcoming: counts.upcoming,
|
||||
overdue: counts.overdue,
|
||||
no_due_date: counts.no_due_date,
|
||||
};
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
private static async groupByDate(currentTab: string,tasks: any[], timeZone: string, today: Date) {
|
||||
const formatToday = moment(today).format("YYYY-MM-DD");
|
||||
|
||||
const tasksReturn = [];
|
||||
|
||||
if (currentTab === this.ALL_TAB) {
|
||||
for (const task of tasks) {
|
||||
tasksReturn.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTab === this.NO_DUE_DATE_TAB) {
|
||||
for (const task of tasks) {
|
||||
if (!task.end_date) {
|
||||
tasksReturn.push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTab === this.TODAY_TAB) {
|
||||
for (const task of tasks) {
|
||||
if (task.end_date) {
|
||||
const taskEndDate = momentTime.tz(task.end_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
if (moment(taskEndDate).isSame(formatToday)) {
|
||||
tasksReturn.push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTab === this.UPCOMING_TAB) {
|
||||
for (const task of tasks) {
|
||||
if (task.end_date) {
|
||||
const taskEndDate = momentTime.tz(task.end_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
if (moment(taskEndDate).isAfter(formatToday)) {
|
||||
tasksReturn.push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTab === this.OVERDUE_TAB) {
|
||||
for (const task of tasks) {
|
||||
if (task.end_date) {
|
||||
const taskEndDate = momentTime.tz(task.end_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
if (moment(taskEndDate).isBefore(formatToday)) {
|
||||
tasksReturn.push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tasksReturn;
|
||||
}
|
||||
|
||||
private static async groupBySingleDate(tasks: any, timeZone: string, selectedDate: string) {
|
||||
const formatSelectedDate = moment(selectedDate).format("YYYY-MM-DD");
|
||||
|
||||
const tasksReturn = [];
|
||||
|
||||
for (const task of tasks) {
|
||||
if (task.end_date) {
|
||||
const taskEndDate = momentTime.tz(task.end_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
if (moment(taskEndDate).isSame(formatSelectedDate)) {
|
||||
tasksReturn.push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tasksReturn;
|
||||
|
||||
}
|
||||
|
||||
private static async getCountsByGroup(tasks: any[], timeZone: string, today_: Date) {
|
||||
let no_due_date = 0;
|
||||
let today = 0;
|
||||
let upcoming = 0;
|
||||
let overdue = 0;
|
||||
|
||||
const total = tasks.length;
|
||||
|
||||
const formatToday = moment(today_).format("YYYY-MM-DD");
|
||||
|
||||
for (const task of tasks) {
|
||||
if (!task.end_date) {
|
||||
no_due_date = no_due_date + 1;
|
||||
}
|
||||
if (task.end_date) {
|
||||
const taskEndDate = momentTime.tz(task.end_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
if (moment(taskEndDate).isSame(formatToday)) {
|
||||
today = today + 1;
|
||||
}
|
||||
if (moment(taskEndDate).isAfter(formatToday)) {
|
||||
upcoming = upcoming + 1;
|
||||
}
|
||||
if (moment(taskEndDate).isBefore(formatToday)) {
|
||||
overdue = overdue + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
total,
|
||||
today,
|
||||
upcoming,
|
||||
overdue,
|
||||
no_due_date
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getPersonalTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const user_id = req.user?.id;
|
||||
const q = `SELECT ptl.id,
|
||||
ptl.name,
|
||||
ptl.created_at,
|
||||
FALSE AS is_task,
|
||||
ptl.done,
|
||||
ptl.updated_at
|
||||
FROM personal_todo_list ptl
|
||||
WHERE ptl.user_id = $1
|
||||
AND done IS FALSE
|
||||
ORDER BY ptl.updated_at DESC`;
|
||||
const results = await db.query(q, [user_id]);
|
||||
return res.status(200).send(new ServerResponse(true, results.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
const team_id = req.user?.team_id;
|
||||
const user_id = req.user?.id;
|
||||
|
||||
const current_view = req.query.view;
|
||||
|
||||
const isFavorites = current_view === "1" ? ` AND EXISTS(SELECT user_id FROM favorite_projects WHERE user_id = $2 AND project_id = projects.id)` : "";
|
||||
const isArchived = req.query.filter === "2"
|
||||
? ` AND EXISTS(SELECT user_id FROM archived_projects WHERE user_id = $2 AND project_id = projects.id)`
|
||||
: ` AND NOT EXISTS(SELECT user_id FROM archived_projects WHERE user_id = $2 AND project_id = projects.id)`;
|
||||
|
||||
const q = `SELECT id,
|
||||
name,
|
||||
EXISTS(SELECT user_id
|
||||
FROM favorite_projects
|
||||
WHERE user_id = $2
|
||||
AND project_id = projects.id) AS favorite,
|
||||
EXISTS(SELECT user_id
|
||||
FROM archived_projects
|
||||
WHERE user_id = $2
|
||||
AND project_id = projects.id) AS archived,
|
||||
color_code,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = projects.id) AS all_tasks_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = projects.id
|
||||
AND status_id IN (SELECT id
|
||||
FROM task_statuses
|
||||
WHERE project_id = projects.id
|
||||
AND category_id IN
|
||||
(SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS completed_tasks_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM project_members
|
||||
WHERE project_id = projects.id) AS members_count,
|
||||
(SELECT get_project_members(projects.id)) AS names,
|
||||
(SELECT CASE
|
||||
WHEN ((SELECT MAX(updated_at)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = projects.id) >
|
||||
updated_at)
|
||||
THEN (SELECT MAX(updated_at)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = projects.id)
|
||||
ELSE updated_at END) AS updated_at
|
||||
FROM projects
|
||||
WHERE team_id = $1 ${isArchived} ${isFavorites} AND is_member_of_project(projects.id , $2
|
||||
, $1)
|
||||
ORDER BY updated_at DESC`;
|
||||
|
||||
const result = await db.query(q, [team_id, user_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjectsByTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const team_id = req.user?.team_id;
|
||||
const user_id = req.user?.id;
|
||||
const q = `
|
||||
SELECT id, name, color_code
|
||||
FROM projects
|
||||
WHERE team_id = $1
|
||||
AND is_member_of_project(projects.id, $2, $1)
|
||||
`;
|
||||
const result = await db.query(q, [team_id, user_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updatePersonalTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
UPDATE personal_todo_list
|
||||
SET done = TRUE
|
||||
WHERE id = $1
|
||||
RETURNING id
|
||||
`;
|
||||
await db.query(q, [req.body.id]);
|
||||
return res.status(200).send(new ServerResponse(true, req.body.id));
|
||||
}
|
||||
}
|
||||
102
worklenz-backend/src/controllers/index-controller.ts
Normal file
102
worklenz-backend/src/controllers/index-controller.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
import {NextFunction} from "express";
|
||||
import FileConstants from "../shared/file-constants";
|
||||
import {isInternalServer, isProduction, log_error} from "../shared/utils";
|
||||
import db from "../config/db";
|
||||
import createHttpError from "http-errors";
|
||||
|
||||
export default class IndexController extends WorklenzControllerBase {
|
||||
public static use(req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction) {
|
||||
try {
|
||||
const url = `https://${req.hostname}${req.url}`;
|
||||
res.locals.release = FileConstants.getRelease();
|
||||
res.locals.user = req.user;
|
||||
res.locals.url = url;
|
||||
res.locals.env = process.env.NODE_ENV;
|
||||
res.locals.isInternalServer = isInternalServer;
|
||||
res.locals.isProduction = isProduction;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
public static async index(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
const q = `SELECT free_tier_storage, team_member_limit, projects_limit, trial_duration FROM licensing_settings;`;
|
||||
const result = await db.query(q, []);
|
||||
const [settings] = result.rows;
|
||||
res.render("index", {settings});
|
||||
}
|
||||
|
||||
public static pricing(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
res.render("pricing");
|
||||
}
|
||||
|
||||
public static privacyPolicy(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
res.render("privacy-policy");
|
||||
}
|
||||
|
||||
public static termsOfUse(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
res.render("terms-of-use");
|
||||
}
|
||||
|
||||
public static admin(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
res.render("admin");
|
||||
}
|
||||
|
||||
public static auth(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
if (req.isAuthenticated())
|
||||
return res.redirect("/worklenz");
|
||||
return res.render("admin");
|
||||
}
|
||||
|
||||
public static worklenz(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
if (req.isAuthenticated())
|
||||
return res.render("admin");
|
||||
|
||||
if (req.user && !req.user.is_member)
|
||||
return res.redirect("/teams");
|
||||
|
||||
return res.redirect("/auth");
|
||||
}
|
||||
|
||||
public static redirectToLogin(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
res.redirect("/auth/login");
|
||||
}
|
||||
|
||||
public static async signup(req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const teamMemberId = req.query.user as string;
|
||||
const q = `SELECT set_active_team_by_member_id($1);`;
|
||||
await db.query(q, [teamMemberId || null]);
|
||||
} catch (error) {
|
||||
log_error(error, req.query);
|
||||
return next(createHttpError(500));
|
||||
}
|
||||
|
||||
if (req.isAuthenticated())
|
||||
return res.redirect("/worklenz");
|
||||
|
||||
return res.render("admin");
|
||||
}
|
||||
|
||||
public static async login(req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction) {
|
||||
// Set active team to invited team
|
||||
try {
|
||||
const teamId = req.query.team as string; // invited team id
|
||||
const userId = req.query.user as string; // invited user's id
|
||||
const q = `SELECT set_active_team($1, $2);`;
|
||||
await db.query(q, [userId || null, teamId || null]);
|
||||
} catch (error) {
|
||||
log_error(error, req.query);
|
||||
return next(createHttpError(500));
|
||||
}
|
||||
|
||||
if (req.isAuthenticated())
|
||||
return res.redirect("/worklenz");
|
||||
|
||||
return res.render("admin");
|
||||
}
|
||||
}
|
||||
62
worklenz-backend/src/controllers/job-titles-controller.ts
Normal file
62
worklenz-backend/src/controllers/job-titles-controller.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class JobTitlesController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {name} = req.body;
|
||||
const q = `INSERT INTO job_titles (name, team_id) VALUES ($1, (SELECT active_team FROM users WHERE id = $2::UUID));`;
|
||||
const result = await db.query(q, [name, req.user?.id || null]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {searchQuery, sortField, sortOrder, size, offset} = this.toPaginationOptions(req.query, "name");
|
||||
|
||||
const q = `
|
||||
SELECT ROW_TO_JSON(rec) AS job_titles
|
||||
FROM (SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT id, name
|
||||
FROM job_titles
|
||||
WHERE team_id = $1 ${searchQuery}
|
||||
ORDER BY ${sortField} ${sortOrder}
|
||||
LIMIT $2 OFFSET $3) t) AS data
|
||||
FROM job_titles
|
||||
WHERE team_id = $1 ${searchQuery}) rec;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.team_id || null, size, offset]);
|
||||
const [data] = result.rows;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.job_titles || this.paginatedDatasetDefaultStruct));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name FROM job_titles WHERE id = $1 AND team_id = $2;`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `UPDATE job_titles SET name = $1 WHERE id = $2 AND team_id = $3;`;
|
||||
const result = await db.query(q, [req.body.name, req.params.id, req.user?.team_id || null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `DELETE FROM job_titles WHERE id = $1 AND team_id = $2;`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
92
worklenz-backend/src/controllers/labels-controller.ts
Normal file
92
worklenz-backend/src/controllers/labels-controller.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {TASK_PRIORITY_COLOR_ALPHA, WorklenzColorCodes} from "../shared/constants";
|
||||
|
||||
export default class LabelsController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
WITH lbs AS (SELECT id,
|
||||
name,
|
||||
color_code,
|
||||
(SELECT COUNT(*) FROM task_labels WHERE label_id = team_labels.id) AS usage,
|
||||
EXISTS(SELECT 1
|
||||
FROM task_labels
|
||||
WHERE task_labels.label_id = team_labels.id
|
||||
AND EXISTS(SELECT 1
|
||||
FROM tasks
|
||||
WHERE id = task_labels.task_id
|
||||
AND project_id = $2)) AS used
|
||||
FROM team_labels
|
||||
WHERE team_id = $1
|
||||
ORDER BY name)
|
||||
SELECT id, name, color_code, usage
|
||||
FROM lbs
|
||||
ORDER BY used DESC;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.team_id, req.query.project || null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getByTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT (SELECT name FROM team_labels WHERE id = task_labels.label_id),
|
||||
(SELECT color_code FROM team_labels WHERE id = task_labels.label_id)
|
||||
FROM task_labels
|
||||
WHERE task_id = $1;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const 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;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id]);
|
||||
|
||||
for (const label of result.rows) {
|
||||
label.color_code = label.color_code + TASK_PRIORITY_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateColor(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `UPDATE team_labels
|
||||
SET color_code = $3
|
||||
WHERE id = $1
|
||||
AND team_id = $2;`;
|
||||
|
||||
if (!WorklenzColorCodes.includes(req.body.color))
|
||||
return res.status(400).send(new ServerResponse(false, null));
|
||||
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id, req.body.color]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `DELETE
|
||||
FROM team_labels
|
||||
WHERE id = $1
|
||||
AND team_id = $2;`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
26
worklenz-backend/src/controllers/logs-controller.ts
Normal file
26
worklenz-backend/src/controllers/logs-controller.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class LogsController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getActivityLog(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT description, (SELECT name FROM projects WHERE projects.id = project_logs.project_id) AS project_name, created_at
|
||||
FROM project_logs
|
||||
WHERE team_id = $1
|
||||
AND (CASE
|
||||
WHEN (is_owner($2, $1) OR
|
||||
is_admin($2, $1)) THEN TRUE
|
||||
ELSE is_member_of_project(project_id, $2, $1) END)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.team_id || null, req.user?.id || null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
151
worklenz-backend/src/controllers/notification-controller.ts
Normal file
151
worklenz-backend/src/controllers/notification-controller.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {getColor} from "../shared/utils";
|
||||
|
||||
export default class NotificationController extends WorklenzControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT un.id,
|
||||
un.message,
|
||||
un.created_at,
|
||||
un.read,
|
||||
(SELECT name FROM teams WHERE id = un.team_id) AS team,
|
||||
(SELECT name FROM projects WHERE id = t.project_id) AS project,
|
||||
(SELECT color_code FROM projects WHERE id = t.project_id) AS color,
|
||||
t.project_id,
|
||||
t.id AS task_id,
|
||||
un.team_id
|
||||
FROM user_notifications un
|
||||
LEFT JOIN tasks t ON un.task_id = t.id
|
||||
WHERE user_id = $1
|
||||
AND read = $2
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 100;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [req.user?.id, req.query.filter === "Read"]);
|
||||
|
||||
for (const item of result.rows) {
|
||||
item.team_color = getColor(item.team_name);
|
||||
item.url = item.project_id ? `/worklenz/projects/${item.project_id}` : null;
|
||||
item.params = {task: item.task_id, tab: "tasks-list"};
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getSettings(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT email_notifications_enabled, popup_notifications_enabled, show_unread_items_count, daily_digest_enabled
|
||||
FROM notification_settings
|
||||
WHERE user_id = $1
|
||||
AND team_id = $2;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.id, req.user?.team_id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const settings = {
|
||||
email_notifications_enabled: !!data?.email_notifications_enabled,
|
||||
popup_notifications_enabled: !!data?.popup_notifications_enabled,
|
||||
show_unread_items_count: !!data?.show_unread_items_count,
|
||||
daily_digest_enabled: !!data?.daily_digest_enabled
|
||||
};
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, settings));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getUnreadCount(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT COALESCE(COUNT(*)::INTEGER, 0) AS notifications_count,
|
||||
(SELECT COALESCE(COUNT(*)::INTEGER, 0) FROM email_invitations WHERE email = (SELECT email FROM users WHERE id = $1)) AS invitations_count
|
||||
FROM user_notifications
|
||||
WHERE user_id = $1
|
||||
AND read = false
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [req.user?.id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data.notifications_count + data.invitations_count));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateSettings(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
UPDATE notification_settings
|
||||
SET email_notifications_enabled = $3,
|
||||
popup_notifications_enabled = $4,
|
||||
show_unread_items_count = $5,
|
||||
daily_digest_enabled = $6
|
||||
WHERE user_id = $1
|
||||
AND team_id = $2
|
||||
RETURNING email_notifications_enabled,
|
||||
popup_notifications_enabled,
|
||||
show_unread_items_count,
|
||||
daily_digest_enabled;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [
|
||||
req.user?.id,
|
||||
req.user?.team_id,
|
||||
!!req.body.email_notifications_enabled,
|
||||
!!req.body.popup_notifications_enabled,
|
||||
!!req.body.show_unread_items_count,
|
||||
!!req.body.daily_digest_enabled
|
||||
]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const settings = {
|
||||
email_notifications_enabled: !!data?.email_notifications_enabled,
|
||||
popup_notifications_enabled: !!data?.popup_notifications_enabled,
|
||||
show_unread_items_count: !!data?.show_unread_items_count,
|
||||
daily_digest_enabled: !!data?.daily_digest_enabled
|
||||
};
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, settings));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
UPDATE user_notifications
|
||||
SET read = TRUE
|
||||
WHERE id = $1
|
||||
AND user_id = $2;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async delete(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
DELETE
|
||||
FROM user_notifications
|
||||
WHERE id = $1
|
||||
AND user_id = $2
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async readAll(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
UPDATE user_notifications
|
||||
SET read = TRUE
|
||||
WHERE user_id = $1
|
||||
AND read IS FALSE;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
50
worklenz-backend/src/controllers/overview-controller.ts
Normal file
50
worklenz-backend/src/controllers/overview-controller.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class OverviewController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT id,
|
||||
name,
|
||||
color_code,
|
||||
notes,
|
||||
(SELECT name FROM clients WHERE id = projects.client_id) AS client_name,
|
||||
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT team_member_id AS id,
|
||||
(SELECT task_id
|
||||
FROM tasks_assignees
|
||||
WHERE EXISTS(SELECT id FROM tasks WHERE project_id = $1)
|
||||
AND project_member_id = id) AS task_count,
|
||||
(SELECT name
|
||||
FROM users
|
||||
WHERE id =
|
||||
(SELECT user_id
|
||||
FROM team_members
|
||||
WHERE team_member_id = project_members.team_member_id)),
|
||||
(SELECT name
|
||||
FROM job_titles
|
||||
WHERE id = (SELECT job_title_id
|
||||
FROM team_members
|
||||
WHERE id = project_members.team_member_id)) AS job_title
|
||||
FROM project_members
|
||||
WHERE project_id = projects.id
|
||||
ORDER BY name ASC) rec) AS members,
|
||||
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT id, name, done FROM tasks WHERE project_id = projects.id ORDER BY name ASC) rec) AS tasks
|
||||
FROM projects
|
||||
WHERE id = $1
|
||||
AND team_id = $2;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class PersonalOverviewController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getTasksDueToday(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT id,
|
||||
name,
|
||||
(SELECT name FROM projects WHERE project_id = projects.id) AS project_name,
|
||||
(SELECT name FROM task_statuses WHERE id = t.status_id) AS status,
|
||||
(SELECT task_priorities.name FROM task_priorities WHERE id = t.priority_id) AS priority,
|
||||
start_date,
|
||||
end_date
|
||||
FROM tasks t
|
||||
JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE t.archived IS FALSE AND t.end_date::DATE = NOW()::DATE
|
||||
AND is_member_of_project(t.project_id, $2, $1)
|
||||
ORDER BY end_date DESC
|
||||
LIMIT 5;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.team_id || null, req.user?.id || null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTasksRemaining(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT id,
|
||||
name,
|
||||
(SELECT name FROM projects WHERE project_id = projects.id) AS project_name,
|
||||
(SELECT name FROM task_statuses WHERE id = t.status_id) AS status,
|
||||
(SELECT task_priorities.name FROM task_priorities WHERE id = t.priority_id) AS priority,
|
||||
start_date,
|
||||
end_date
|
||||
FROM tasks t
|
||||
JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE t.archived IS FALSE AND t.end_date::DATE > NOW()::DATE
|
||||
AND is_member_of_project(t.project_id, $2, $1)
|
||||
ORDER BY end_date DESC
|
||||
LIMIT 5;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.team_id || null, req.user?.id || null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTaskOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT id,
|
||||
name,
|
||||
color_code,
|
||||
(SELECT MIN(start_date) FROM tasks WHERE archived IS FALSE AND project_id = projects.id) AS min_date,
|
||||
(SELECT MAX(end_date) FROM tasks WHERE archived IS FALSE AND project_id = projects.id) AS max_date
|
||||
FROM projects
|
||||
WHERE team_id = $1
|
||||
AND (CASE
|
||||
WHEN (is_owner($2, $1) OR
|
||||
is_admin($2, $1)) THEN TRUE
|
||||
ELSE is_member_of_project(projects.id, $2,
|
||||
$1) END)
|
||||
ORDER BY NAME;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.team_id || null, req.user?.id || null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import db from "../config/db";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {IPassportSession} from "../interfaces/passport-session";
|
||||
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import {NotificationsService} from "../services/notifications/notifications.service";
|
||||
import {slugify} from "../shared/utils";
|
||||
import {generateProjectKey} from "../utils/generate-project-key";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
|
||||
export default class ProfileSettingsController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async setup(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT complete_account_setup($1, $2, $3) AS account;`;
|
||||
req.body.key = generateProjectKey(req.body.project_name, []) || null;
|
||||
const result = await db.query(q, [req.user?.id, req.user?.team_id, JSON.stringify(req.body)]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (!data)
|
||||
return res.status(200).send(new ServerResponse(false, null, "Account setup failed! Please try again"));
|
||||
|
||||
const newMembers = data.account.members || [];
|
||||
|
||||
NotificationsService.sendTeamMembersInvitations(newMembers, req.user as IPassportSession);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.account));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT name, email
|
||||
FROM users
|
||||
WHERE id = $1;`;
|
||||
const result = await db.query(q, [req.user?.id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `UPDATE users
|
||||
SET name = $2,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
RETURNING id, name, email;`;
|
||||
const result = await db.query(q, [req.user?.id, req.body.name]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update_team_name(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT update_team_name($1);`;
|
||||
const body = {
|
||||
id: req.params.id,
|
||||
name: req.body.name,
|
||||
key: slugify(req.body.name),
|
||||
user_id: req.user?.id
|
||||
};
|
||||
await db.query(q, [JSON.stringify(body)]);
|
||||
return res.status(200).send(new ServerResponse(true, body));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import { getColor } from "../shared/utils";
|
||||
import { WorklenzColorCodes } from "../shared/constants";
|
||||
|
||||
export default class ProjectCategoriesController extends WorklenzControllerBase {
|
||||
|
||||
private static flatString(text: string) {
|
||||
return (text || "").split(",").map(s => `'${s}'`).join(",");
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
INSERT INTO project_categories (name, team_id, created_by, color_code)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, name, color_code;
|
||||
`;
|
||||
const name = req.body.name.trim();
|
||||
const result = await db.query(q, [name, req.user?.team_id, req.user?.id, name ? getColor(name) : null]);
|
||||
const [data] = result.rows;
|
||||
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 projects WHERE category_id = project_categories.id) AS usage
|
||||
FROM project_categories
|
||||
WHERE team_id = $1;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.team_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT id, name, color_code, (SELECT COUNT(*) FROM projects WHERE category_id = project_categories.id) AS usage
|
||||
FROM project_categories
|
||||
WHERE team_id = $1;`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
private static async getTeamsByOrg(teamId: string) {
|
||||
const q = `SELECT id FROM teams WHERE in_organization(id, $1)`;
|
||||
const result = await db.query(q, [teamId]);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getByMultipleTeams(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
const teams = await this.getTeamsByOrg(req.user?.team_id as string);
|
||||
const teamIds = teams.map(team => team.id).join(",");
|
||||
|
||||
const q = `SELECT id, name, color_code FROM project_categories WHERE team_id IN (${this.flatString(teamIds)});`;
|
||||
|
||||
const result = await db.query(q);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
UPDATE project_categories
|
||||
SET color_code = $2
|
||||
WHERE id = $1
|
||||
AND team_id = $3;
|
||||
`;
|
||||
|
||||
if (!WorklenzColorCodes.includes(req.body.color))
|
||||
return res.status(400).send(new ServerResponse(false, null));
|
||||
|
||||
const result = await db.query(q, [req.params.id, req.body.color, req.user?.team_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
DELETE
|
||||
FROM project_categories
|
||||
WHERE id = $1
|
||||
AND team_id = $2;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
241
worklenz-backend/src/controllers/project-comments-controller.ts
Normal file
241
worklenz-backend/src/controllers/project-comments-controller.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {getColor, slugify} from "../shared/utils";
|
||||
import { HTML_TAG_REGEXP } from "../shared/constants";
|
||||
import { IProjectCommentEmailNotification } from "../interfaces/comment-email-notification";
|
||||
import { sendProjectComment } from "../shared/email-notifications";
|
||||
import { NotificationsService } from "../services/notifications/notifications.service";
|
||||
import { IO } from "../shared/io";
|
||||
import { SocketEvents } from "../socket.io/events";
|
||||
|
||||
interface IMailConfig {
|
||||
message: string;
|
||||
receiverEmail: string;
|
||||
receiverName: string;
|
||||
content: string;
|
||||
teamName: string;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
interface IMention {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class ProjectCommentsController extends WorklenzControllerBase {
|
||||
|
||||
private static replaceContent(messageContent: string, mentions: { id: string; name: string }[]) {
|
||||
const mentionNames = mentions.map(mention => mention.name);
|
||||
|
||||
const replacedContent = mentionNames.reduce(
|
||||
(content, mentionName, index) => {
|
||||
const regex = new RegExp(`@${mentionName}`, "g");
|
||||
return content.replace(regex, `{${index}}`);
|
||||
},
|
||||
messageContent
|
||||
);
|
||||
|
||||
return replacedContent;
|
||||
}
|
||||
|
||||
private static async sendMail(config: IMailConfig) {
|
||||
const subject = config.message.replace(HTML_TAG_REGEXP, "");
|
||||
|
||||
const data: IProjectCommentEmailNotification = {
|
||||
greeting: `Hi ${config.receiverName}`,
|
||||
summary: subject,
|
||||
team: config.teamName,
|
||||
project_name: config.projectName,
|
||||
comment: config.content
|
||||
};
|
||||
|
||||
await sendProjectComment(config.receiverEmail, data);
|
||||
}
|
||||
|
||||
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const userId = req.user?.id;
|
||||
const mentions: IMention[] = req.body.mentions;
|
||||
const projectId = req.body.project_id;
|
||||
const teamId = req.user?.team_id;
|
||||
|
||||
let commentContent = req.body.content;
|
||||
if (mentions.length > 0) {
|
||||
commentContent = await this.replaceContent(commentContent, mentions);
|
||||
}
|
||||
|
||||
const body = {
|
||||
project_id : projectId,
|
||||
created_by: userId,
|
||||
content: commentContent,
|
||||
mentions,
|
||||
team_id: teamId
|
||||
};
|
||||
|
||||
const q = `SELECT create_project_comment($1) AS comment`;
|
||||
const result = await db.query(q, [JSON.stringify(body)]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const projectMembers = await this.getMembersList(projectId);
|
||||
|
||||
const commentMessage = `<b>${req.user?.name}</b> added a comment on <b>${data.comment.project_name}</b> (${data.comment.team_name})`;
|
||||
|
||||
for (const member of projectMembers || []) {
|
||||
if (member.id && member.id === req.user?.id) continue;
|
||||
NotificationsService.createNotification({
|
||||
userId: member.id,
|
||||
teamId: req.user?.team_id as string,
|
||||
socketId: member.socket_id,
|
||||
message: commentMessage,
|
||||
taskId: null,
|
||||
projectId
|
||||
});
|
||||
if (member.id !== req.user?.id && member.socket_id) {
|
||||
IO.emit(SocketEvents.NEW_PROJECT_COMMENT_RECEIVED, member.socket_id, true);
|
||||
}
|
||||
}
|
||||
|
||||
const mentionMessage = `<b>${req.user?.name}</b> has mentioned you in a comment on <b>${data.comment.project_name}</b> (${data.comment.team_name})`;
|
||||
const rdMentions = [...new Set(req.body.mentions || [])] as IMention[]; // remove duplicates
|
||||
|
||||
for (const mention of rdMentions) {
|
||||
if (mention) {
|
||||
const member = await this.getUserDataByUserId(mention.id, projectId, teamId as string);
|
||||
NotificationsService.sendNotification({
|
||||
team: data.comment.team_name,
|
||||
receiver_socket_id: member.socket_id,
|
||||
message: mentionMessage,
|
||||
task_id: "",
|
||||
project_id: projectId,
|
||||
project: data.comment.project_name,
|
||||
project_color: member.project_color,
|
||||
team_id: req.user?.team_id as string
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
private static async getUserDataByUserId(informedBy: string, projectId: string, team_id: string) {
|
||||
const q = `
|
||||
SELECT id,
|
||||
name,
|
||||
email,
|
||||
socket_id,
|
||||
(SELECT email_notifications_enabled
|
||||
FROM notification_settings
|
||||
WHERE notification_settings.team_id = $3
|
||||
AND notification_settings.user_id = $1),
|
||||
(SELECT color_code FROM projects WHERE id = $2) AS project_color
|
||||
FROM users
|
||||
WHERE id = $1;
|
||||
`;
|
||||
const result = await db.query(q, [informedBy, projectId, team_id]);
|
||||
const [data] = result.rows;
|
||||
return data;
|
||||
}
|
||||
|
||||
private static async getMembersList(projectId: string) {
|
||||
const q = `
|
||||
SELECT
|
||||
tm.user_id AS id,
|
||||
(SELECT name
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = tm.id),
|
||||
(SELECT email
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = tm.id) AS email,
|
||||
(SELECT socket_id FROM users WHERE users.id = tm.user_id) AS socket_id,
|
||||
(SELECT email_notifications_enabled
|
||||
FROM notification_settings
|
||||
WHERE team_id = tm.team_id
|
||||
AND notification_settings.user_id = tm.user_id) AS email_notifications_enabled
|
||||
FROM project_members
|
||||
INNER JOIN team_members tm ON project_members.team_member_id = tm.id
|
||||
LEFT JOIN users u ON tm.user_id = u.id
|
||||
WHERE project_id = $1 AND tm.user_id IS NOT NULL
|
||||
ORDER BY name
|
||||
`;
|
||||
const result = await db.query(q, [projectId]);
|
||||
const members = result.rows;
|
||||
return members;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const members = await this.getMembersList(req.params.id as string);
|
||||
return res.status(200).send(new ServerResponse(true, members || this.paginatedDatasetDefaultStruct));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
const limit = req.query.isLimit;
|
||||
|
||||
const q = `
|
||||
SELECT
|
||||
pc.id,
|
||||
pc.content AS content,
|
||||
(SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
|
||||
FROM (SELECT u.name AS user_name,
|
||||
u.email AS user_email
|
||||
FROM project_comment_mentions pcm
|
||||
LEFT JOIN users u ON pcm.informed_by = u.id
|
||||
WHERE pcm.comment_id = pc.id) rec) AS mentions,
|
||||
(SELECT id FROM users WHERE id = pc.created_by) AS user_id,
|
||||
(SELECT name FROM users WHERE id = pc.created_by) AS created_by,
|
||||
(SELECT avatar_url FROM users WHERE id = pc.created_by),
|
||||
pc.created_at,
|
||||
pc.updated_at
|
||||
FROM project_comments pc
|
||||
WHERE pc.project_id = $1 ORDER BY pc.updated_at DESC
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
|
||||
const data = result.rows;
|
||||
|
||||
for (const comment of data) {
|
||||
const {mentions} = comment;
|
||||
if (mentions.length > 0) {
|
||||
const placeHolders = comment.content.match(/{\d+}/g);
|
||||
if (placeHolders) {
|
||||
comment.content = await comment.content.replace(/\n/g, "</br>");
|
||||
placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
|
||||
const index = parseInt(placeHolder.match(/\d+/)[0]);
|
||||
if (index >= 0 && index < comment.mentions.length) {
|
||||
comment.content = comment.content.replace(placeHolder, `<span class='mentions'>@${comment.mentions[index].user_name}</span>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
const color_code = getColor(comment.created_by);
|
||||
comment.color_code = color_code;
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getCountByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT COUNT(*) AS total FROM project_comments WHERE project_id = $1`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, parseInt(data.total)));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `DELETE FROM project_comments WHERE id = $1 RETURNING id`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
}
|
||||
103
worklenz-backend/src/controllers/project-folders-controller.ts
Normal file
103
worklenz-backend/src/controllers/project-folders-controller.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {slugify} from "../shared/utils";
|
||||
import {IProjectFolder} from "../interfaces/project-folder";
|
||||
|
||||
export default class ProjectFoldersController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
INSERT INTO project_folders (name, key, created_by, team_id, color_code)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, name, key, color_code;
|
||||
`;
|
||||
|
||||
const name = req.body.name?.trim() || null;
|
||||
const key = slugify(name);
|
||||
const createdBy = req.user?.id ?? null;
|
||||
const teamId = req.user?.team_id ?? null;
|
||||
const colorCode = req.body.color_code?.trim() || "#70a6f3";
|
||||
|
||||
const result = await db.query(q, [name, key, createdBy, teamId, colorCode]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse<IProjectFolder>(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const parentFolderId = (req.query.parent as string)?.trim() || null;
|
||||
|
||||
const q = [
|
||||
`SELECT id,
|
||||
name,
|
||||
key,
|
||||
color_code,
|
||||
created_at,
|
||||
(SELECT name
|
||||
FROM team_member_info_view
|
||||
WHERE user_id = project_folders.created_by
|
||||
AND team_member_info_view.team_id = project_folders.team_id) AS created_by
|
||||
FROM project_folders
|
||||
WHERE team_id = $1
|
||||
`,
|
||||
parentFolderId ? `AND parent_folder_id = $2` : "",
|
||||
`ORDER BY name;`
|
||||
].join(" ");
|
||||
const params = parentFolderId ? [req.user?.team_id, parentFolderId] : [req.user?.team_id];
|
||||
|
||||
const result = await db.query(q, params);
|
||||
return res.status(200).send(new ServerResponse<IProjectFolder[]>(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT id, key, name, color_code
|
||||
FROM project_folders
|
||||
WHERE key = $1
|
||||
AND team_id = $2;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse<IProjectFolder>(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
UPDATE project_folders
|
||||
SET name = $2,
|
||||
key = $3,
|
||||
color_code = COALESCE($5, color_code),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
AND team_id = $4
|
||||
RETURNING id, name, key;
|
||||
`;
|
||||
|
||||
const name = req.body.name?.trim() || null;
|
||||
const key = slugify(name);
|
||||
const colorCode = req.body.color_code?.trim() || null;
|
||||
|
||||
const result = await db.query(q, [req.params.id, name, key, req.user?.team_id, colorCode]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse<IProjectFolder>(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
DELETE
|
||||
FROM project_folders
|
||||
WHERE id = $1
|
||||
AND team_id = $2;
|
||||
`;
|
||||
await db.query(q, [req.params.id, req.user?.team_id]);
|
||||
return res.status(200).send(new ServerResponse(true, null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class ProjectHealthController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async get(_req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name, color_code, is_default FROM sys_project_healths ORDER BY sort_order;`;
|
||||
const result = await db.query(q, []);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
343
worklenz-backend/src/controllers/project-insights-controller.ts
Normal file
343
worklenz-backend/src/controllers/project-insights-controller.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
import {PriorityColorCodes, TASK_STATUS_COLOR_ALPHA} from "../shared/constants";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {formatDuration, getColor} from "../shared/utils";
|
||||
import moment from "moment";
|
||||
|
||||
export default class ProjectInsightsController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {archived} = req.query;
|
||||
|
||||
const q = `SELECT get_project_overview_data($1, $2) AS overview;`;
|
||||
const result = await db.query(q, [req.params.id, archived === "true"]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const {total_minutes_sum, time_spent_sum} = data.overview;
|
||||
|
||||
const totalMinutes = moment.duration(total_minutes_sum, "minutes");
|
||||
const totalSeconds = moment.duration(time_spent_sum, "seconds");
|
||||
|
||||
data.overview.total_estimated_hours_string = formatDuration(totalMinutes);
|
||||
data.overview.total_logged_hours_string = formatDuration(totalSeconds);
|
||||
|
||||
data.overview.overlogged_hours = formatDuration(totalMinutes.subtract(totalSeconds));
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.overview));
|
||||
}
|
||||
|
||||
public static async getMemberInsightsByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {archived} = req.query;
|
||||
|
||||
const q = `SELECT get_project_member_insights($1, $2) AS overview;`;
|
||||
const result = await db.query(q, [req.params.id, archived === "true"]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data.overview));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getLastUpdatedtasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {archived} = req.query;
|
||||
|
||||
const q = `SELECT get_last_updated_tasks_by_project($1, $2, $3, $4) AS last_updated;`;
|
||||
const result = await db.query(q, [req.params.id, 10, 0, archived]);
|
||||
const [data] = result.rows;
|
||||
|
||||
for (const task of data.last_updated) {
|
||||
task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.last_updated));
|
||||
}
|
||||
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjectLogs(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT description, created_at
|
||||
FROM project_logs
|
||||
WHERE project_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3;`;
|
||||
const result = await db.query(q, [req.params.id, 10, 0]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows || []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getStatusOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {archived} = req.query;
|
||||
|
||||
const q = `
|
||||
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 = $1
|
||||
AND team_id = $2
|
||||
ORDER BY task_statuses.sort_order;`;
|
||||
const status = await db.query(q, [req.params.id, req.user?.team_id]);
|
||||
const statusCounts = [];
|
||||
|
||||
for (const element of status.rows) {
|
||||
const q = `SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE status_id = $1
|
||||
AND CASE
|
||||
WHEN ($2 IS TRUE) THEN project_id IS NOT NULL
|
||||
ELSE archived IS FALSE END;`;
|
||||
const count = await db.query(q, [element.id, archived === "true"]);
|
||||
const [data] = count.rows;
|
||||
statusCounts.push({name: element.name, color: element.color_code, y: parseInt(data.count)});
|
||||
element.status_color = element.status_color + TASK_STATUS_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, statusCounts || []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getPriorityOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {archived} = req.query;
|
||||
|
||||
const q = `SELECT id, name, value
|
||||
FROM task_priorities
|
||||
ORDER BY value;`;
|
||||
const result = await db.query(q, []);
|
||||
for (const item of result.rows)
|
||||
item.color_code = PriorityColorCodes[item.value] || PriorityColorCodes["0"];
|
||||
|
||||
const statusCounts = [];
|
||||
|
||||
for (const element of result.rows) {
|
||||
const q = `SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE priority_id = $1
|
||||
AND CASE
|
||||
WHEN ($3 IS TRUE) THEN project_id IS NOT NULL
|
||||
ELSE archived IS FALSE END
|
||||
AND project_id = $2;`;
|
||||
const count = await db.query(q, [element.id, req.params.id, archived === "true"]);
|
||||
const [data] = count.rows;
|
||||
statusCounts.push({name: element.name, color: element.color_code, data: [parseInt(data.count)]});
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, statusCounts || []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getOverdueTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {archived} = req.query;
|
||||
|
||||
const q = `
|
||||
SELECT id,
|
||||
name,
|
||||
status_id AS status,
|
||||
end_date,
|
||||
priority_id AS priority,
|
||||
(SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status_name,
|
||||
updated_at,
|
||||
NOW()::DATE - end_date::DATE AS days_overdue,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND end_date::DATE < NOW()::DATE
|
||||
AND CASE
|
||||
WHEN ($2 IS TRUE) THEN project_id IS NOT NULL
|
||||
ELSE archived IS FALSE END
|
||||
AND status_id IN (SELECT id
|
||||
FROM task_statuses
|
||||
WHERE project_id = $1
|
||||
AND category_id IN
|
||||
(SELECT id
|
||||
FROM sys_task_status_categories
|
||||
WHERE sys_task_status_categories.is_done IS FALSE));
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, archived]);
|
||||
|
||||
for (const element of result.rows) {
|
||||
element.status_color = element.status_color + TASK_STATUS_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows || []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTasksFinishedEarly(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {archived} = req.query;
|
||||
|
||||
const q = `
|
||||
SELECT id,
|
||||
name,
|
||||
status_id AS status,
|
||||
end_date,
|
||||
priority_id AS priority,
|
||||
(SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status_name,
|
||||
updated_at,
|
||||
completed_at,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND completed_at::DATE < end_date::DATE
|
||||
AND CASE
|
||||
WHEN ($2 IS TRUE) THEN project_id IS NOT NULL
|
||||
ELSE archived IS FALSE END
|
||||
AND status_id IN (SELECT id
|
||||
FROM task_statuses
|
||||
WHERE project_id = $1
|
||||
AND category_id IN
|
||||
(SELECT id
|
||||
FROM sys_task_status_categories
|
||||
WHERE sys_task_status_categories.is_done IS TRUE));
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, archived]);
|
||||
|
||||
for (const element of result.rows) {
|
||||
element.status_color = element.status_color + TASK_STATUS_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows || []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTasksFinishedLate(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {archived} = req.query;
|
||||
|
||||
const q = `
|
||||
SELECT id,
|
||||
name,
|
||||
status_id AS status,
|
||||
end_date,
|
||||
priority_id AS priority,
|
||||
(SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status_name,
|
||||
updated_at,
|
||||
completed_at,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND completed_at::DATE > end_date::DATE
|
||||
AND CASE
|
||||
WHEN ($2 IS TRUE) THEN project_id IS NOT NULL
|
||||
ELSE archived IS FALSE END
|
||||
AND status_id IN (SELECT id
|
||||
FROM task_statuses
|
||||
WHERE project_id = $1
|
||||
AND category_id IN
|
||||
(SELECT id
|
||||
FROM sys_task_status_categories
|
||||
WHERE sys_task_status_categories.is_done IS TRUE));
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, archived]);
|
||||
|
||||
for (const element of result.rows) {
|
||||
element.status_color = element.status_color + TASK_STATUS_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows || []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTasksByProjectMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {member_id, project_id, archived} = req.body;
|
||||
const q = `SELECT get_tasks_by_project_member($1, $2, $3)`;
|
||||
const result = await db.query(q, [project_id || null, member_id || null, archived]);
|
||||
const [data] = result.rows;
|
||||
|
||||
for (const element of data.get_tasks_by_project_member) {
|
||||
element.status_color = element.status_color + TASK_STATUS_COLOR_ALPHA;
|
||||
element.total_minutes = formatDuration(moment.duration(~~(element.total_minutes), "minutes"));
|
||||
element.overlogged_time = formatDuration(moment.duration(element.overlogged_time, "seconds"));
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.get_tasks_by_project_member || []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjectDeadlineStats(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {archived} = req.query;
|
||||
|
||||
const q = `SELECT get_project_deadline_tasks($1, $2);`;
|
||||
const result = await db.query(q, [req.params.id || null, archived === "true"]);
|
||||
const [data] = result.rows;
|
||||
|
||||
for (const task of data.get_project_deadline_tasks.tasks) {
|
||||
task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
const logged_hours = data.get_project_deadline_tasks.deadline_logged_hours || 0; // in seconds
|
||||
data.get_project_deadline_tasks.deadline_logged_hours_string = formatDuration(moment.duration(logged_hours, "seconds"));
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.get_project_deadline_tasks || {}));
|
||||
}
|
||||
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getOverloggedTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {archived} = req.query;
|
||||
|
||||
/**
|
||||
SELECT id,
|
||||
name,
|
||||
status_id AS status,
|
||||
(SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status_name,
|
||||
end_date,
|
||||
priority_id AS priority,
|
||||
updated_at,
|
||||
((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = tasks.id) - total_minutes) AS overlogged_time,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color,
|
||||
(SELECT get_task_assignees(tasks.id)) AS assignees
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND CASE
|
||||
WHEN ($2 IS TRUE) THEN project_id IS NOT NULL
|
||||
ELSE archived IS FALSE END
|
||||
AND total_minutes < (SELECT SUM(time_spent) FROM task_work_log WHERE task_id = tasks.id);
|
||||
*/
|
||||
const q = `
|
||||
WITH work_log AS (SELECT task_id, SUM(time_spent) AS total_time_spent
|
||||
FROM task_work_log
|
||||
GROUP BY task_id)
|
||||
SELECT id,
|
||||
name,
|
||||
status_id AS status,
|
||||
(SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status_name,
|
||||
end_date,
|
||||
priority_id AS priority,
|
||||
updated_at,
|
||||
(work_log.total_time_spent - (total_minutes * 60)) AS overlogged_time,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color,
|
||||
(SELECT get_task_assignees(tasks.id)) AS assignees
|
||||
FROM tasks
|
||||
JOIN work_log ON work_log.task_id = tasks.id
|
||||
WHERE project_id = $1
|
||||
AND CASE
|
||||
WHEN ($2 IS TRUE) THEN project_id IS NOT NULL
|
||||
ELSE archived IS FALSE END
|
||||
AND total_minutes < work_log.total_time_spent;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [req.params.id || null, archived]);
|
||||
|
||||
for (const task of result.rows) {
|
||||
task.overlogged_time_string = formatDuration(moment.duration(task.overlogged_time, "seconds"));
|
||||
task.assignees.map((a: any) => a.color_code = getColor(a.name));
|
||||
task.names = this.createTagList(task.assignees);
|
||||
task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows || []));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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";
|
||||
|
||||
|
||||
export default class ProjectManagersController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getByOrg(_req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
// const q = `SELECT DISTINCT (SELECT user_id from team_member_info_view tmv WHERE tmv.team_member_id = pm.team_member_id),
|
||||
// team_member_id,
|
||||
// (SELECT name from team_member_info_view tmv WHERE tmv.team_member_id = pm.team_member_id)
|
||||
// FROM project_members pm
|
||||
// WHERE project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')
|
||||
// AND pm.project_id IN (SELECT id FROM projects WHERE team_id IN (SELECT id FROM teams WHERE in_organization(id, $1)));`;
|
||||
// const q = `SELECT DISTINCT tmv.user_id,
|
||||
// tmv.name,
|
||||
// pm.team_member_id
|
||||
// FROM team_member_info_view tmv
|
||||
// INNER JOIN project_members pm ON tmv.team_member_id = pm.team_member_id
|
||||
// INNER JOIN projects p ON pm.project_id = p.id
|
||||
// WHERE pm.project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')
|
||||
// AND p.team_id IN (SELECT id FROM teams WHERE in_organization(id, $1))`;
|
||||
const q = `SELECT DISTINCT ON (tm.user_id)
|
||||
tm.user_id AS id,
|
||||
u.name,
|
||||
pm.team_member_id
|
||||
FROM
|
||||
projects p
|
||||
JOIN project_members pm ON p.id = pm.project_id
|
||||
JOIN teams t ON p.team_id = t.id
|
||||
JOIN team_members tm ON pm.team_member_id = tm.id
|
||||
JOIN team_member_info_view tmi ON tm.id = tmi.team_member_id
|
||||
JOIN users u ON tm.user_id = u.id
|
||||
WHERE
|
||||
t.id IN (SELECT id FROM teams WHERE in_organization(id, $1))
|
||||
AND pm.project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')
|
||||
GROUP BY
|
||||
tm.user_id, u.name, pm.team_member_id`;
|
||||
const result = await db.query(q, [_req.user?.team_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
150
worklenz-backend/src/controllers/project-members-controller.ts
Normal file
150
worklenz-backend/src/controllers/project-members-controller.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {getColor} from "../shared/utils";
|
||||
import TeamMembersController from "./team-members-controller";
|
||||
import {NotificationsService} from "../services/notifications/notifications.service";
|
||||
|
||||
export default class ProjectMembersController extends WorklenzControllerBase {
|
||||
|
||||
public static async checkIfUserAlreadyExists(owner_id: string, email: string) {
|
||||
if (!owner_id) throw new Error("Owner not found.");
|
||||
|
||||
const q = `SELECT EXISTS(SELECT tmi.team_member_id
|
||||
FROM team_member_info_view AS tmi
|
||||
JOIN teams AS t ON tmi.team_id = t.id
|
||||
WHERE tmi.email = $1::TEXT
|
||||
AND t.user_id = $2::UUID);`;
|
||||
const result = await db.query(q, [email, owner_id]);
|
||||
|
||||
const [data] = result.rows;
|
||||
return data.exists;
|
||||
}
|
||||
|
||||
public static async createOrInviteMembers(body: any) {
|
||||
if (!body) return;
|
||||
|
||||
const q = `SELECT create_project_member($1) AS res;`;
|
||||
|
||||
const result = await db.query(q, [JSON.stringify(body)]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const response = data.res;
|
||||
|
||||
if (response?.notification && response?.member_user_id) {
|
||||
NotificationsService.sendNotification({
|
||||
receiver_socket_id: response.socket_id,
|
||||
project: response.project,
|
||||
message: response.notification,
|
||||
project_color: response.project_color,
|
||||
project_id: response.project_id,
|
||||
team: response.team,
|
||||
team_id: body.team_id
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
req.body.user_id = req.user?.id;
|
||||
req.body.team_id = req.user?.team_id;
|
||||
req.body.access_level = req.body.access_level ? req.body.access_level : "MEMBER";
|
||||
const data = await this.createOrInviteMembers(req.body);
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions({
|
||||
raisedExceptions: {
|
||||
"ERROR_EMAIL_INVITATION_EXISTS": "Member already have a pending invitation that has not been accepted."
|
||||
}
|
||||
})
|
||||
public static async createByEmail(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
req.body.user_id = req.user?.id;
|
||||
req.body.team_id = req.user?.team_id;
|
||||
|
||||
if (!req.user?.team_id) return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
|
||||
|
||||
// Adding as a team member
|
||||
const teamMemberReq: { team_id?: string; emails: string[], project_id?: string; } = {
|
||||
team_id: req.user?.team_id,
|
||||
emails: [req.body.email]
|
||||
};
|
||||
|
||||
if (req.body.project_id)
|
||||
teamMemberReq.project_id = req.body.project_id;
|
||||
|
||||
const [member] = await TeamMembersController.createOrInviteMembers(teamMemberReq, req.user);
|
||||
|
||||
if (!member)
|
||||
return res.status(200).send(new ServerResponse(true, null, "Failed to add the member to the project. Please try again."));
|
||||
|
||||
// Adding to the project
|
||||
const projectMemberReq = {
|
||||
team_member_id: member.team_member_id,
|
||||
team_id: req.user?.team_id,
|
||||
project_id: req.body.project_id,
|
||||
user_id: req.user?.id,
|
||||
access_level: req.body.access_level ? req.body.access_level : "MEMBER"
|
||||
};
|
||||
const data = await this.createOrInviteMembers(projectMemberReq);
|
||||
return res.status(200).send(new ServerResponse(true, data.member));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT project_members.id,
|
||||
tm.id AS team_member_id,
|
||||
(SELECT email FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id),
|
||||
(SELECT name FROM team_member_info_view WHERE team_member_id = project_members.team_member_id) AS name,
|
||||
u.avatar_url,
|
||||
jt.name AS job_title
|
||||
FROM project_members
|
||||
INNER JOIN team_members tm ON project_members.team_member_id = tm.id
|
||||
LEFT JOIN job_titles jt ON tm.job_title_id = jt.id
|
||||
LEFT JOIN users u ON tm.user_id = u.id
|
||||
WHERE project_id = $1
|
||||
ORDER BY project_members.created_at DESC;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
|
||||
result.rows.forEach((a: any) => a.color_code = getColor(a.name));
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
public static async checkIfMemberExists(projectId: string, teamMemberId: string) {
|
||||
const q = `SELECT EXISTS(SELECT id FROM project_members WHERE project_id = $1::UUID AND team_member_id = $2::UUID)`;
|
||||
const result = await db.query(q, [projectId, teamMemberId]);
|
||||
const [data] = result.rows;
|
||||
return data.exists;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT remove_project_member($1, $2, $3) AS res;`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.id, req.user?.team_id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const response = data.res;
|
||||
|
||||
if (response?.notification && response?.member_user_id) {
|
||||
NotificationsService.sendNotification({
|
||||
receiver_socket_id: response.socket_id,
|
||||
project: response.project,
|
||||
message: response.notification,
|
||||
project_color: response.project_color,
|
||||
project_id: response.project_id,
|
||||
team: response.team,
|
||||
team_id: req.user?.team_id as string
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import moment, { Moment } from "moment";
|
||||
import WorklenzControllerBase from "../worklenz-controller-base";
|
||||
import momentTime from "moment-timezone";
|
||||
|
||||
export const GroupBy = {
|
||||
STATUS: "status",
|
||||
PRIORITY: "priority",
|
||||
LABELS: "labels",
|
||||
PHASE: "phase"
|
||||
};
|
||||
|
||||
export interface IRMTaskGroup {
|
||||
id?: string;
|
||||
name: string;
|
||||
color_code: string;
|
||||
category_id: string | null;
|
||||
old_category_id?: string;
|
||||
tasks: any[];
|
||||
is_expanded: boolean;
|
||||
}
|
||||
|
||||
export default class RoadmapTasksControllerV2Base extends WorklenzControllerBase {
|
||||
|
||||
public static updateTaskViewModel(task: any, globalStartDate: Moment, globalDateWidth: number , timeZone: string) {
|
||||
|
||||
if (typeof task.sub_tasks_count === "undefined") task.sub_tasks_count = "0";
|
||||
|
||||
task.is_sub_task = !!task.parent_task_id;
|
||||
|
||||
task.show_sub_tasks = false;
|
||||
|
||||
if (task.start_date)
|
||||
task.start_date = momentTime.tz(task.start_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
|
||||
if (task.end_date)
|
||||
task.end_date = momentTime.tz(task.end_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
|
||||
this.setTaskCss(task, globalStartDate, globalDateWidth);
|
||||
|
||||
task.isVisible = true;
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
private static setTaskCss(task: any, globalStartDate: Moment, globalDateWidth: number ) {
|
||||
let startDate = task.start_date ? moment(task.start_date).format("YYYY-MM-DD") : moment();
|
||||
let endDate = task.end_date ? moment(task.end_date).format("YYYY-MM-DD") : moment();
|
||||
|
||||
if (!task.start_date) {
|
||||
startDate = moment(task.end_date).format("YYYY-MM-DD");
|
||||
}
|
||||
if (!task.end_date) {
|
||||
endDate = moment(task.start_date).format("YYYY-MM-DD");
|
||||
}
|
||||
if (!task.start_date && !task.end_date) {
|
||||
startDate = moment().format("YYYY-MM-DD");
|
||||
endDate = moment().format("YYYY-MM-DD");
|
||||
}
|
||||
|
||||
const fStartDate = moment(startDate);
|
||||
const fEndDate = moment(endDate);
|
||||
const fGlobalStartDate = moment(globalStartDate).format("YYYY-MM-DD");
|
||||
|
||||
const daysDifferenceFromStart = fStartDate.diff(fGlobalStartDate, "days");
|
||||
task.offset_from = daysDifferenceFromStart * globalDateWidth;
|
||||
|
||||
if (moment(fStartDate).isSame(moment(fEndDate), "day")) {
|
||||
task.width = globalDateWidth;
|
||||
} else {
|
||||
const taskWidth = fEndDate.diff(fStartDate, "days");
|
||||
task.width = (taskWidth + 1) * globalDateWidth;
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
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 RoadmapTasksControllerV2Base, {GroupBy, IRMTaskGroup} from "./roadmap-tasks-contoller-v2-base";
|
||||
import moment, {Moment} from "moment";
|
||||
import momentTime from "moment-timezone";
|
||||
|
||||
export class TaskListGroup implements IRMTaskGroup {
|
||||
name: string;
|
||||
category_id: string | null;
|
||||
color_code: string;
|
||||
tasks: any[];
|
||||
is_expanded: 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.is_expanded = group.is_expanded;
|
||||
}
|
||||
}
|
||||
|
||||
export default class RoadmapTasksControllerV2 extends RoadmapTasksControllerV2Base {
|
||||
|
||||
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");
|
||||
|
||||
private static async getFirstLastDates(projectId: string) {
|
||||
|
||||
const q = `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 = $1 AND tasks.archived IS FALSE
|
||||
UNION
|
||||
SELECT MIN(end_date) AS min_date, MAX(end_date) AS max_date
|
||||
FROM tasks
|
||||
WHERE project_id = $1 AND tasks.archived IS FALSE) AS date_union;`;
|
||||
|
||||
const res = await db.query(q, [projectId]);
|
||||
return res.rows[0];
|
||||
}
|
||||
|
||||
private static validateEndDate(endDate: Moment): boolean {
|
||||
return moment(endDate.format("YYYY-MM-DD")).isBefore(moment(), "day");
|
||||
}
|
||||
|
||||
private static validateStartDate(startDate: Moment): boolean {
|
||||
return moment(startDate.format("YYYY-MM-DD")).isBefore(moment(), "day");
|
||||
}
|
||||
|
||||
private static getScrollAmount(startDate: Moment) {
|
||||
const today = moment().format("YYYY-MM-DD");
|
||||
const daysDifference = moment(today).diff(startDate, "days");
|
||||
|
||||
return (this.GLOBAL_DATE_WIDTH * daysDifference);
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async createDateRange(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
const dateRange = await this.getFirstLastDates(req.params.id as string);
|
||||
|
||||
const today = new Date();
|
||||
|
||||
let startDate = moment(today).clone().startOf("month");
|
||||
let endDate = moment(today).clone().endOf("month");
|
||||
|
||||
if (dateRange.start_date)
|
||||
dateRange.start_date = momentTime.tz(dateRange.start_date, `${req.query.timeZone}`).format("YYYY-MM-DD");
|
||||
|
||||
if (dateRange.end_date)
|
||||
dateRange.end_date = momentTime.tz(dateRange.end_date, `${req.query.timeZone}`).format("YYYY-MM-DD");
|
||||
|
||||
if (dateRange.start_date && dateRange.end_date) {
|
||||
startDate = this.validateStartDate(moment(dateRange.start_date)) ? moment(dateRange.start_date).startOf("month") : moment(today).clone().startOf("month");
|
||||
endDate = this.validateEndDate(moment(dateRange.end_date)) ? moment(today).clone().endOf("month") : moment(dateRange.end_date).endOf("month");
|
||||
} else if (dateRange.start_date && !dateRange.end_date) {
|
||||
startDate = this.validateStartDate(moment(dateRange.start_date)) ? moment(dateRange.start_date).startOf("month") : moment(today).clone().startOf("month");
|
||||
} else if (!dateRange.start_date && dateRange.end_date) {
|
||||
endDate = this.validateEndDate(moment(dateRange.end_date)) ? moment(today).clone().endOf("month") : moment(dateRange.end_date).endOf("month");
|
||||
}
|
||||
|
||||
const xMonthsBeforeStart = startDate.clone().subtract(2, "months");
|
||||
const xMonthsAfterEnd = endDate.clone().add(3, "months");
|
||||
|
||||
this.GLOBAL_START_DATE = moment(xMonthsBeforeStart).format("YYYY-MM-DD");
|
||||
this.GLOBAL_END_DATE = moment(xMonthsAfterEnd).format("YYYY-MM-DD");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const scrollBy = 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 isCountsOnly(query: ParsedQs) {
|
||||
return query.count === "true";
|
||||
}
|
||||
|
||||
public static isTasksOnlyReq(query: ParsedQs) {
|
||||
return RoadmapTasksControllerV2.isCountsOnly(query) || query.parent_task;
|
||||
}
|
||||
|
||||
|
||||
private static getQuery(userId: string, options: ParsedQs) {
|
||||
const searchField = options.search ? "t.name" : "sort_order";
|
||||
const {searchQuery} = RoadmapTasksControllerV2.toPaginationOptions(options, searchField);
|
||||
|
||||
const isSubTasks = !!options.parent_task;
|
||||
|
||||
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),
|
||||
].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 COUNT(*)
|
||||
FROM tasks
|
||||
WHERE parent_task_id = t.id)::INT AS sub_tasks_count,
|
||||
|
||||
t.status_id AS status,
|
||||
t.archived,
|
||||
|
||||
(SELECT phase_id FROM task_phase WHERE task_id = t.id) AS phase_id,
|
||||
|
||||
(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 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,
|
||||
start_date,
|
||||
end_date
|
||||
FROM tasks t
|
||||
WHERE ${filters} ${searchQuery} AND project_id = $1
|
||||
ORDER BY t.start_date ASC NULLS LAST`;
|
||||
}
|
||||
|
||||
public static async getGroups(groupBy: string, projectId: string): Promise<IRMTaskGroup[]> {
|
||||
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);
|
||||
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 = RoadmapTasksControllerV2.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]: IRMTaskGroup }, group) => {
|
||||
if (group.id)
|
||||
g[group.id] = new TaskListGroup(group);
|
||||
return g;
|
||||
}, {});
|
||||
|
||||
this.updateMapByGroup(tasks, groupBy, map, req.query.expandedGroups as string, req.query.timezone as string);
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
|
||||
if (req.query.expandedGroups) {
|
||||
const expandedGroup = updatedGroups.find(g => g.id === req.query.expandedGroups);
|
||||
if (expandedGroup) expandedGroup.is_expanded = true;
|
||||
} else {
|
||||
updatedGroups[0].is_expanded = true;
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, updatedGroups));
|
||||
}
|
||||
|
||||
public static updateMapByGroup(tasks: any[], groupBy: string, map: {
|
||||
[p: string]: IRMTaskGroup
|
||||
}, expandedGroup: string, timeZone: string) {
|
||||
let index = 0;
|
||||
const unmapped = [];
|
||||
for (const task of tasks) {
|
||||
task.index = index++;
|
||||
RoadmapTasksControllerV2.updateTaskViewModel(task, moment(this.GLOBAL_START_DATE), this.GLOBAL_DATE_WIDTH, timeZone);
|
||||
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,
|
||||
is_expanded: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTasksOnly(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const isSubTasks = !!req.query.parent_task;
|
||||
const q = RoadmapTasksControllerV2.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) {
|
||||
RoadmapTasksControllerV2.updateTaskViewModel(task, moment(this.GLOBAL_START_DATE), this.GLOBAL_DATE_WIDTH, req.query.timeZone as string);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class ProjectstatusesController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async get(_req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name, color_code, icon, is_default FROM sys_project_statuses ORDER BY sort_order;`;
|
||||
const result = await db.query(q, []);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
@@ -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 }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA } from "../../shared/constants";
|
||||
import { getColor } from "../../shared/utils";
|
||||
import WorklenzControllerBase from ".././worklenz-controller-base";
|
||||
|
||||
export const GroupBy = {
|
||||
STATUS: "status",
|
||||
PRIORITY: "priority",
|
||||
LABELS: "labels",
|
||||
PHASE: "phase"
|
||||
};
|
||||
|
||||
export interface IWLTaskGroup {
|
||||
id?: string;
|
||||
name: string;
|
||||
color_code: string;
|
||||
category_id: string | null;
|
||||
old_category_id?: string;
|
||||
tasks: any[];
|
||||
isExpand: boolean;
|
||||
}
|
||||
|
||||
export default class WLTasksControllerBase extends WorklenzControllerBase {
|
||||
protected static calculateTaskCompleteRatio(totalCompleted: number, totalTasks: number) {
|
||||
if (totalCompleted === 0 && totalTasks === 0) return 0;
|
||||
const ratio = ((totalCompleted / totalTasks) * 100);
|
||||
return ratio == Infinity ? 100 : ratio.toFixed();
|
||||
}
|
||||
|
||||
public static updateTaskViewModel(task: any) {
|
||||
task.progress = ~~(task.total_minutes_spent / task.total_minutes * 100);
|
||||
task.overdue = task.total_minutes < task.total_minutes_spent;
|
||||
|
||||
if (typeof task.sub_tasks_count === "undefined") task.sub_tasks_count = "0";
|
||||
|
||||
task.is_sub_task = !!task.parent_task_id;
|
||||
|
||||
task.name_color = getColor(task.name);
|
||||
task.priority_color = PriorityColorCodes[task.priority_value] || PriorityColorCodes["0"];
|
||||
task.show_sub_tasks = false;
|
||||
|
||||
if (task.phase_id) {
|
||||
task.phase_color = task.phase_name
|
||||
? getColor(task.phase_name) + TASK_PRIORITY_COLOR_ALPHA
|
||||
: null;
|
||||
}
|
||||
|
||||
if (Array.isArray(task.assignees)) {
|
||||
for (const assignee of task.assignees) {
|
||||
assignee.color_code = getColor(assignee.name);
|
||||
}
|
||||
}
|
||||
|
||||
task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
|
||||
task.priority_color = task.priority_color + TASK_PRIORITY_COLOR_ALPHA;
|
||||
|
||||
const totalCompleted = +task.completed_sub_tasks + +task.parent_task_completed;
|
||||
const totalTasks = +task.sub_tasks_count + 1; // +1 for parent
|
||||
task.complete_ratio = WLTasksControllerBase.calculateTaskCompleteRatio(totalCompleted, totalTasks);
|
||||
task.completed_count = totalCompleted;
|
||||
task.total_tasks_count = totalTasks;
|
||||
|
||||
return task;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,844 @@
|
||||
import moment, { Moment } from "moment";
|
||||
import momentTime from "moment-timezone";
|
||||
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 WLTasksControllerBase, { GroupBy, IWLTaskGroup } from "./workload-gannt-base";
|
||||
|
||||
interface IWorkloadTask {
|
||||
id: string;
|
||||
name: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
width: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
export class IWLTaskListGroup implements IWLTaskGroup {
|
||||
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 WorkloadGanntController extends WLTasksControllerBase {
|
||||
|
||||
private static GLOBAL_DATE_WIDTH = 30;
|
||||
private static GLOBAL_START_DATE = moment().format("YYYY-MM-DD");
|
||||
private static GLOBAL_END_DATE = moment().format("YYYY-MM-DD");
|
||||
|
||||
private static TASKS_START_DATE_NULL_FILTER = "start_date_null";
|
||||
private static TASKS_END_DATE_NULL_FILTER = "end_date_null";
|
||||
private static TASKS_START_END_DATES_NULL_FILTER = "start_end_dates_null";
|
||||
|
||||
private static async getFirstLastDates(projectId: string) {
|
||||
|
||||
const q = `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 = $1 AND tasks.archived IS FALSE
|
||||
UNION
|
||||
SELECT MIN(end_date) AS min_date, MAX(end_date) AS max_date
|
||||
FROM tasks
|
||||
WHERE project_id = $1 AND tasks.archived IS FALSE) AS date_union;`;
|
||||
|
||||
const res = await db.query(q, [projectId]);
|
||||
return res.rows[0];
|
||||
}
|
||||
|
||||
private static async getLogsFirstLastDates(projectId: string) {
|
||||
const q = `SELECT MIN(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS min_date,
|
||||
MAX(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS max_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 = $1`;
|
||||
const res = await db.query(q, [projectId]);
|
||||
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 setTaskCss(task: IWorkloadTask) {
|
||||
let startDate = task.start_date ? moment(task.start_date) : moment();
|
||||
let endDate = task.end_date ? moment(task.end_date) : moment();
|
||||
|
||||
if (!task.start_date) {
|
||||
startDate = moment(task.end_date);
|
||||
}
|
||||
if (!task.end_date) {
|
||||
endDate = moment(task.start_date);
|
||||
}
|
||||
if (!task.start_date && !task.end_date) {
|
||||
startDate = moment();
|
||||
endDate = moment();
|
||||
}
|
||||
|
||||
const daysDifferenceFromStart = startDate.diff(this.GLOBAL_START_DATE, "days");
|
||||
task.left = daysDifferenceFromStart * this.GLOBAL_DATE_WIDTH;
|
||||
|
||||
if (moment(startDate).isSame(moment(endDate), "day")) {
|
||||
task.width = this.GLOBAL_DATE_WIDTH;
|
||||
} else {
|
||||
const taskWidth = endDate.diff(startDate, "days");
|
||||
task.width = (taskWidth + 1) * this.GLOBAL_DATE_WIDTH;
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
private static setIndicator(startDate: string, endDate: string) {
|
||||
const daysFromStart = moment(startDate).diff(this.GLOBAL_START_DATE, "days");
|
||||
const indicatorOffset = daysFromStart * this.GLOBAL_DATE_WIDTH;
|
||||
|
||||
const daysDifference = moment(endDate).diff(startDate, "days");
|
||||
const indicatorWidth = (daysDifference + 1) * this.GLOBAL_DATE_WIDTH;
|
||||
|
||||
const body = {
|
||||
indicatorOffset,
|
||||
indicatorWidth
|
||||
};
|
||||
|
||||
return body;
|
||||
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async createDateRange(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
const dateRange = await this.getFirstLastDates(req.params.id as string);
|
||||
const logRange = await this.getLogsFirstLastDates(req.params.id as string);
|
||||
|
||||
const today = new Date();
|
||||
|
||||
let startDate = moment(today).clone().startOf("month");
|
||||
let endDate = moment(today).clone().endOf("month");
|
||||
|
||||
this.setChartStartEnd(dateRange, logRange, req.query.timeZone as string);
|
||||
|
||||
if (dateRange.start_date && dateRange.end_date) {
|
||||
startDate = this.validateStartDate(moment(dateRange.start_date)) ? moment(dateRange.start_date).startOf("month") : moment(today).clone().startOf("month");
|
||||
endDate = this.validateEndDate(moment(dateRange.end_date)) ? moment(today).clone().endOf("month") : moment(dateRange.end_date).endOf("month");
|
||||
} else if (dateRange.start_date && !dateRange.end_date) {
|
||||
startDate = this.validateStartDate(moment(dateRange.start_date)) ? moment(dateRange.start_date).startOf("month") : moment(today).clone().startOf("month");
|
||||
} else if (!dateRange.start_date && dateRange.end_date) {
|
||||
endDate = this.validateEndDate(moment(dateRange.end_date)) ? moment(today).clone().endOf("month") : moment(dateRange.end_date).endOf("month");
|
||||
}
|
||||
|
||||
const xMonthsBeforeStart = startDate.clone().subtract(1, "months");
|
||||
const xMonthsAfterEnd = endDate.clone().add(1, "months");
|
||||
|
||||
this.GLOBAL_START_DATE = moment(xMonthsBeforeStart).format("YYYY-MM-DD");
|
||||
this.GLOBAL_END_DATE = moment(xMonthsAfterEnd).format("YYYY-MM-DD");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const scrollBy = 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 setChartStartEnd(dateRange: any, logsRange: any, timeZone: string) {
|
||||
|
||||
if (dateRange.start_date)
|
||||
dateRange.start_date = momentTime.tz(dateRange.start_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
|
||||
if (dateRange.end_date)
|
||||
dateRange.end_date = momentTime.tz(dateRange.end_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
|
||||
if (logsRange.min_date)
|
||||
logsRange.min_date = momentTime.tz(logsRange.min_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
|
||||
if (logsRange.max_date)
|
||||
logsRange.max_date = momentTime.tz(logsRange.max_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
|
||||
if (moment(logsRange.min_date ).isBefore(dateRange.start_date))
|
||||
dateRange.start_date = logsRange.min_date;
|
||||
|
||||
if (moment(logsRange.max_date ).isAfter(dateRange.endDate))
|
||||
dateRange.end_date = logsRange.max_date;
|
||||
|
||||
return dateRange;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
const expandedMembers: string[] = req.body.expanded_members;
|
||||
|
||||
const q = `SELECT pm.id AS project_member_id,
|
||||
tmiv.team_member_id,
|
||||
tmiv.user_id,
|
||||
name AS name,
|
||||
avatar_url,
|
||||
TRUE AS project_member,
|
||||
|
||||
(SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
|
||||
FROM (SELECT MIN(LEAST(start_date, end_date)) AS min_date,
|
||||
MAX(GREATEST(start_date, end_date)) AS max_date
|
||||
FROM tasks
|
||||
INNER JOIN tasks_assignees ta ON tasks.id = ta.task_id
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = $1
|
||||
AND ta.team_member_id = tmiv.team_member_id) rec) AS duration,
|
||||
|
||||
(SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
|
||||
FROM (SELECT MIN(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS min_date,
|
||||
MAX(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS max_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 = $1
|
||||
AND twl.user_id = tmiv.user_id) rec) AS logs_date_union,
|
||||
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT start_date,
|
||||
end_date
|
||||
FROM tasks
|
||||
INNER JOIN tasks_assignees ta ON tasks.id = ta.task_id
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = pm.project_id
|
||||
AND ta.team_member_id = tmiv.team_member_id
|
||||
ORDER BY start_date ASC) rec) AS tasks
|
||||
FROM project_members pm
|
||||
INNER JOIN team_member_info_view tmiv ON pm.team_member_id = tmiv.team_member_id
|
||||
WHERE project_id = $1
|
||||
ORDER BY (SELECT MIN(LEAST(start_date, end_date))
|
||||
FROM tasks t
|
||||
INNER JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE t.archived IS FALSE
|
||||
AND t.project_id = $1
|
||||
AND ta.team_member_id = tmiv.team_member_id) ASC NULLS LAST`;
|
||||
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
|
||||
for (const member of result.rows) {
|
||||
member.color_code = getColor(member.TaskName);
|
||||
|
||||
this.setMaxMinDate(member, req.query.timeZone as string);
|
||||
|
||||
// if (member.duration[0].min_date)
|
||||
// member.duration[0].min_date = momentTime.tz(member.duration[0].min_date, `${req.query.timeZone}`).format("YYYY-MM-DD");
|
||||
|
||||
// if (member.duration[0].max_date)
|
||||
// member.duration[0].max_date = momentTime.tz(member.duration[0].max_date, `${req.query.timeZone}`).format("YYYY-MM-DD");
|
||||
|
||||
const fStartDate = member.duration.min_date ? moment(member.duration.min_date).format("YYYY-MM-DD") : moment().format("YYYY-MM-DD");
|
||||
const fEndDate = member.duration.max_date ? moment(member.duration.max_date).format("YYYY-MM-DD") : moment().format("YYYY-MM-DD");
|
||||
|
||||
if (member.tasks.length > 0) {
|
||||
const styles = this.setIndicator(fStartDate, fEndDate);
|
||||
member.indicator_offset = styles.indicatorOffset;
|
||||
member.indicator_width = styles.indicatorWidth;
|
||||
member.not_allocated = false;
|
||||
} else {
|
||||
member.indicator_offset = 0;
|
||||
member.indicator_width = 0;
|
||||
member.not_allocated = true;
|
||||
}
|
||||
|
||||
member.tasks_start_date = member.duration.min_date;
|
||||
member.tasks_end_date = member.duration.max_date;
|
||||
member.tasks_stats = await WorkloadGanntController.getMemberTasksStats(member.tasks);
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
private static async setMaxMinDate(member: any, timeZone: string) {
|
||||
|
||||
if (member.duration.min_date)
|
||||
member.duration.min_date = momentTime.tz(member.duration.min_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
|
||||
if (member.duration.max_date)
|
||||
member.duration.max_date = momentTime.tz(member.duration.max_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
|
||||
if (member.duration.min_date && member.duration.max_date && member.logs_date_union.min_date && member.logs_date_union.max_date) {
|
||||
|
||||
const durationMin = momentTime.tz(member.duration.min_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
const durationMax = momentTime.tz(member.duration.max_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
const logMin = momentTime.tz(member.logs_date_union.min_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
const logMax = momentTime.tz(member.logs_date_union.max_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
|
||||
if (moment(logMin).isBefore(durationMin)) {
|
||||
member.duration.min_date = logMin;
|
||||
}
|
||||
if (moment(logMax).isAfter(durationMax)) {
|
||||
member.duration.max_date = logMax;
|
||||
}
|
||||
|
||||
return member;
|
||||
|
||||
}
|
||||
|
||||
if (!member.duration.min_date && !member.duration.max_date && member.logs_date_union.min_date && member.logs_date_union.max_date) {
|
||||
|
||||
const logMin = momentTime.tz(member.logs_date_union.min_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
const logMax = momentTime.tz(member.logs_date_union.max_date, `${timeZone}`).format("YYYY-MM-DD");
|
||||
|
||||
member.duration.min_date = logMin;
|
||||
member.duration.max_date = logMax;
|
||||
|
||||
return member;
|
||||
}
|
||||
|
||||
return member;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static async getMemberTasksStats(tasks: { start_date: string | null, end_date: string | null }[]) {
|
||||
const tasksCount = tasks.length;
|
||||
let nullStartCount = 0;
|
||||
let nullEndCount = 0;
|
||||
let nullBothCount = 0;
|
||||
|
||||
for (const task of tasks) {
|
||||
if ((!task.start_date || task.start_date.trim() === "") && (!task.end_date || task.end_date.trim() === "")) {
|
||||
nullBothCount++;
|
||||
} else if ((!task.start_date || task.start_date.trim() === "") && (task.end_date)) {
|
||||
nullStartCount++;
|
||||
} else if ((!task.end_date || task.end_date.trim() === "") && (task.start_date)) {
|
||||
nullEndCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const body = {
|
||||
total: tasksCount,
|
||||
null_start_dates: nullStartCount,
|
||||
null_end_dates: nullEndCount,
|
||||
null_start_end_dates: nullBothCount,
|
||||
null_start_dates_percentage: (nullStartCount / tasksCount) * 100,
|
||||
null_end_dates_percentage: (nullEndCount / tasksCount) * 100,
|
||||
null_start_end_dates_percentage: (nullBothCount / tasksCount) * 100,
|
||||
available_start_end_dates_percentage: ((tasksCount - (nullStartCount + nullEndCount + nullBothCount)) / tasksCount) * 100
|
||||
};
|
||||
return body;
|
||||
}
|
||||
|
||||
// ********************************************
|
||||
|
||||
private static isCountsOnly(query: ParsedQs) {
|
||||
return query.count === "true";
|
||||
}
|
||||
|
||||
public static isTasksOnlyReq(query: ParsedQs) {
|
||||
return WorkloadGanntController.isCountsOnly(query) || query.parent_task;
|
||||
}
|
||||
|
||||
private static flatString(text: string) {
|
||||
return (text || "").split(" ").map(s => `'${s}'`).join(",");
|
||||
}
|
||||
|
||||
private static getFilterByDatesWhereClosure(text: string) {
|
||||
let closure = "";
|
||||
switch (text.trim()) {
|
||||
case "":
|
||||
closure = ``;
|
||||
break;
|
||||
case WorkloadGanntController.TASKS_START_DATE_NULL_FILTER:
|
||||
closure = `start_date IS NULL AND end_date IS NOT NULL`;
|
||||
break;
|
||||
case WorkloadGanntController.TASKS_END_DATE_NULL_FILTER:
|
||||
closure = `start_date IS NOT NULL AND end_date IS NULL`;
|
||||
break;
|
||||
case WorkloadGanntController.TASKS_START_END_DATES_NULL_FILTER:
|
||||
closure = `start_date IS NULL AND end_date IS NULL`;
|
||||
break;
|
||||
}
|
||||
return closure;
|
||||
}
|
||||
|
||||
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 } = WorkloadGanntController.toPaginationOptions(options, searchField);
|
||||
|
||||
const isSubTasks = !!options.parent_task;
|
||||
|
||||
const sortFields = sortField.replace(/ascend/g, "ASC").replace(/descend/g, "DESC") || "sort_order";
|
||||
// Filter tasks by its members
|
||||
const membersFilter = WorkloadGanntController.getFilterByMembersWhereClosure(options.members as string);
|
||||
// Returns statuses of each task as a json array if filterBy === "member"
|
||||
const statusesQuery = WorkloadGanntController.getStatusesQuery(options.filterBy as string);
|
||||
|
||||
const archivedFilter = options.archived === "true" ? "archived IS TRUE" : "archived IS FALSE";
|
||||
|
||||
const datesFilter = WorkloadGanntController.getFilterByDatesWhereClosure(options.dateChecker as string);
|
||||
|
||||
|
||||
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,
|
||||
datesFilter
|
||||
].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<IWLTaskGroup[]> {
|
||||
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 = WorkloadGanntController.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]: IWLTaskGroup }, group) => {
|
||||
if (group.id)
|
||||
g[group.id] = new IWLTaskListGroup(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]: IWLTaskGroup }) {
|
||||
let index = 0;
|
||||
const unmapped = [];
|
||||
for (const task of tasks) {
|
||||
task.index = index++;
|
||||
WorkloadGanntController.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 = WorkloadGanntController.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) {
|
||||
WorkloadGanntController.updateTaskViewModel(task);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getMemberOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
const projectId = req.params.id;
|
||||
const teamMemberId = req.query.team_member_id;
|
||||
|
||||
const getCountByStatus = await WorkloadGanntController.getTasksCountsByStatus(projectId, teamMemberId as string);
|
||||
const getCountByPriority = await WorkloadGanntController.getTasksCountsByPriority(projectId, teamMemberId as string);
|
||||
const getCountByPhase = await WorkloadGanntController.getTasksCountsByPhase(projectId, teamMemberId as string);
|
||||
const getCountByDates = await WorkloadGanntController.getTasksCountsByDates(projectId, teamMemberId as string);
|
||||
const data = {
|
||||
by_status: getCountByStatus,
|
||||
by_priority: getCountByPriority,
|
||||
by_phase: getCountByPhase,
|
||||
by_dates: getCountByDates
|
||||
};
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
private static async getTasksCountsByStatus(projectId: string, teamMemberId: string) {
|
||||
const q = `SELECT ts.id,
|
||||
ts.name AS label,
|
||||
(SELECT color_code FROM sys_task_status_categories WHERE id = ts.category_id) AS color_code,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT COUNT(*)
|
||||
FROM tasks t
|
||||
WHERE t.project_id = $1
|
||||
AND t.archived IS FALSE
|
||||
AND t.id IN (SELECT task_id
|
||||
FROM tasks_assignees
|
||||
WHERE team_member_id = $2)
|
||||
AND t.status_id = ts.id) rec) AS counts
|
||||
FROM task_statuses ts
|
||||
WHERE project_id = $1`;
|
||||
const res = await db.query(q, [projectId, teamMemberId]);
|
||||
for (const row of res.rows) {
|
||||
row.tasks_count = row.counts[0].count;
|
||||
}
|
||||
return res.rows;
|
||||
}
|
||||
|
||||
private static async getTasksCountsByPriority(projectId: string, teamMemberId: string) {
|
||||
const q = `SELECT tp.id,
|
||||
tp.name AS label,
|
||||
tp.color_code,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT COUNT(*)
|
||||
FROM tasks t
|
||||
WHERE t.project_id = $1
|
||||
AND t.archived IS FALSE
|
||||
AND t.id IN (SELECT task_id
|
||||
FROM tasks_assignees
|
||||
WHERE team_member_id = $2)
|
||||
AND t.priority_id = tp.id) rec) AS counts
|
||||
FROM task_priorities tp`;
|
||||
const res = await db.query(q, [projectId, teamMemberId]);
|
||||
for (const row of res.rows) {
|
||||
row.tasks_count = row.counts[0].count;
|
||||
}
|
||||
return res.rows;
|
||||
}
|
||||
|
||||
private static async getTasksCountsByPhase(projectId: string, teamMemberId: string) {
|
||||
const q = `SELECT pp.id,
|
||||
pp.name AS label,
|
||||
pp.color_code AS color_code,
|
||||
COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON) AS counts
|
||||
FROM project_phases pp
|
||||
LEFT JOIN (SELECT pp.id AS phase_id,
|
||||
COUNT(ta.task_id) AS task_count
|
||||
FROM project_phases pp
|
||||
LEFT JOIN task_phase tp ON pp.id = tp.phase_id
|
||||
LEFT JOIN tasks t ON tp.task_id = t.id
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id AND ta.team_member_id = $2
|
||||
WHERE pp.project_id = $1
|
||||
GROUP BY pp.id ) rec ON pp.id = rec.phase_id
|
||||
WHERE pp.project_id = $1
|
||||
GROUP BY pp.id`;
|
||||
const res = await db.query(q, [projectId, teamMemberId]);
|
||||
for (const row of res.rows) {
|
||||
row.tasks_count = row.counts[0].task_count;
|
||||
row.color_code = getColor(row.label) + TASK_PRIORITY_COLOR_ALPHA;
|
||||
}
|
||||
return res.rows;
|
||||
}
|
||||
|
||||
private static async getTasksCountsByDates(projectId: string, teamMemberId: string) {
|
||||
const q = `SELECT JSON_BUILD_OBJECT(
|
||||
'having_start_end_date', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND archived IS FALSE
|
||||
AND id IN (SELECT task_id
|
||||
FROM tasks_assignees
|
||||
WHERE team_member_id = $2)
|
||||
AND end_date IS NOT NULL AND start_date IS NOT NULL),
|
||||
'no_end_date', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND archived IS FALSE
|
||||
AND id IN (SELECT task_id
|
||||
FROM tasks_assignees
|
||||
WHERE team_member_id = $2)
|
||||
AND end_date IS NULL AND start_date IS NOT NULL),
|
||||
'no_start_date', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND archived IS FALSE
|
||||
AND id IN (SELECT task_id
|
||||
FROM tasks_assignees
|
||||
WHERE team_member_id = $2)
|
||||
AND end_date IS NOT NULL AND start_date IS NULL),
|
||||
'no_start_end_dates', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND archived IS FALSE
|
||||
AND id IN (SELECT task_id
|
||||
FROM tasks_assignees
|
||||
WHERE team_member_id = $2)
|
||||
AND end_date IS NULL AND start_date IS NULL)) AS counts`;
|
||||
const res = await db.query(q, [projectId, teamMemberId]);
|
||||
const data = [
|
||||
{
|
||||
id: "",
|
||||
label: "Having start & end date",
|
||||
color_code: "#f0f0f0",
|
||||
tasks_count: res.rows[0].counts.having_start_end_date
|
||||
},
|
||||
{
|
||||
id: "",
|
||||
label: "Without end date",
|
||||
color_code: "#F9A0A0BF",
|
||||
tasks_count: res.rows[0].counts.no_end_date
|
||||
},
|
||||
{
|
||||
id: "",
|
||||
label: "Without start date",
|
||||
color_code: "#F8A9A98C",
|
||||
tasks_count: res.rows[0].counts.no_start_date
|
||||
},
|
||||
{
|
||||
id: "",
|
||||
label: "Without start & end date",
|
||||
color_code: "#F7A7A7E5",
|
||||
tasks_count: res.rows[0].counts.no_start_end_dates
|
||||
},
|
||||
];
|
||||
return data;
|
||||
}
|
||||
|
||||
// @HandleExceptions()
|
||||
// public static async getTasksByTeamMeberId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
// const memberTasks = await this.getMemberTasks(req.params.id, req.body.team_member_id);
|
||||
// return res.status(200).send(new ServerResponse(true, memberTasks));
|
||||
// }
|
||||
|
||||
// private static async getMemberTasks(projectId: string, teamMemberId: string) {
|
||||
// const q = `
|
||||
// SELECT id AS task_id,
|
||||
// name AS task_name,
|
||||
// start_date AS 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 = $1
|
||||
// AND ta.team_member_id = $2
|
||||
// ORDER BY start_date ASC`;
|
||||
// const result = await db.query(q, [projectId, teamMemberId]);
|
||||
|
||||
// for (const task of result.rows) {
|
||||
// this.setTaskCss(task);
|
||||
// }
|
||||
|
||||
// return result.rows;
|
||||
|
||||
// }
|
||||
|
||||
}
|
||||
744
worklenz-backend/src/controllers/projects-controller.ts
Normal file
744
worklenz-backend/src/controllers/projects-controller.ts
Normal file
@@ -0,0 +1,744 @@
|
||||
import moment from "moment";
|
||||
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 {LOG_DESCRIPTIONS} from "../shared/constants";
|
||||
import {getColor} from "../shared/utils";
|
||||
import {generateProjectKey} from "../utils/generate-project-key";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import { NotificationsService } from "../services/notifications/notifications.service";
|
||||
import { IPassportSession } from "../interfaces/passport-session";
|
||||
import { SocketEvents } from "../socket.io/events";
|
||||
import { IO } from "../shared/io";
|
||||
|
||||
export default class ProjectsController extends WorklenzControllerBase {
|
||||
|
||||
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 notifyProjecManagertUpdates(projectId: string, user: IPassportSession, projectManagerTeamMemberId: string | null) {
|
||||
|
||||
if (projectManagerTeamMemberId) {
|
||||
const q = `SELECT (SELECT user_id FROM team_member_info_view WHERE team_member_id = $2) AS user_id,
|
||||
(SELECT socket_id FROM users WHERE id = (SELECT user_id FROM team_member_info_view WHERE team_member_id = $2)) AS socket_id,
|
||||
(SELECT name FROM projects WHERE id = $1) AS project_name
|
||||
FROM project_members pm WHERE project_id = $1
|
||||
AND project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')`;
|
||||
|
||||
const result = await db.query(q, [projectId, projectManagerTeamMemberId]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (projectManagerTeamMemberId !== user.team_member_id) {
|
||||
void NotificationsService.createNotification({
|
||||
userId: data.user_id,
|
||||
teamId: user?.team_id as string,
|
||||
socketId: data.socket_id,
|
||||
message: `You're assigned as the <b> Project Manager </b> of the <b> ${data.project_name} </b>.`,
|
||||
taskId: null,
|
||||
projectId: projectId as string
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
IO.getSocketById(user.socket_id as string)
|
||||
?.to(projectId)
|
||||
.emit(SocketEvents.PROJECT_DATA_CHANGE.toString(), {user_id: user.id});
|
||||
|
||||
}
|
||||
|
||||
@HandleExceptions({
|
||||
raisedExceptions: {
|
||||
"PROJECT_EXISTS_ERROR": `A project with the name "{0}" already exists. Please choose a different name.`
|
||||
}
|
||||
})
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT create_project($1) AS project`;
|
||||
|
||||
req.body.team_id = req.user?.team_id || null;
|
||||
req.body.user_id = req.user?.id || null;
|
||||
|
||||
req.body.folder_id = req.body.folder_id || null;
|
||||
req.body.category_id = req.body.category_id?.trim() || null;
|
||||
req.body.client_name = req.body.client_name?.trim() || null;
|
||||
req.body.project_created_log = LOG_DESCRIPTIONS.PROJECT_CREATED;
|
||||
req.body.project_member_added_log = LOG_DESCRIPTIONS.PROJECT_MEMBER_ADDED;
|
||||
req.body.project_manager_id = req.body.project_manager ? req.body.project_manager.id : null;
|
||||
|
||||
const keys = await this.getAllKeysByTeamId(req.user?.team_id as string);
|
||||
req.body.key = generateProjectKey(req.body.name, keys) || null;
|
||||
|
||||
const result = await db.query(q, [JSON.stringify(req.body)]);
|
||||
const [data] = result.rows;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.project || {}));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updatePinnedView(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const projectId = req.body.project_id;
|
||||
const teamMemberId = req.user?.team_member_id;
|
||||
const defaultView = req.body.default_view;
|
||||
|
||||
const q = `UPDATE project_members SET default_view = $1 WHERE project_id = $2 AND team_member_id = $3`;
|
||||
const result = await db.query(q, [defaultView, projectId, teamMemberId]);
|
||||
const [data] = result.rows;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getMyProjectsToTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name, color_code
|
||||
FROM projects
|
||||
WHERE team_id = $1
|
||||
AND is_member_of_project(projects.id, $2, $1)`;
|
||||
const result = await db.query(q, [req.user?.team_id, req.user?.id || null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getMyProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {searchQuery, size, offset} = this.toPaginationOptions(req.query, "name");
|
||||
|
||||
const isFavorites = req.query.filter === "1" ? ` AND EXISTS(SELECT user_id FROM favorite_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)` : "";
|
||||
|
||||
const isArchived = req.query.filter === "2"
|
||||
? ` AND EXISTS(SELECT user_id FROM archived_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)`
|
||||
: ` AND NOT EXISTS(SELECT user_id FROM archived_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)`;
|
||||
const q = `
|
||||
SELECT ROW_TO_JSON(rec) AS projects
|
||||
FROM (SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT id,
|
||||
name,
|
||||
EXISTS(SELECT user_id
|
||||
FROM favorite_projects
|
||||
WHERE user_id = '${req.user?.id}'
|
||||
AND project_id = projects.id) AS favorite,
|
||||
EXISTS(SELECT user_id
|
||||
FROM archived_projects
|
||||
WHERE user_id = '${req.user?.id}'
|
||||
AND project_id = projects.id) AS archived,
|
||||
color_code,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = projects.id) AS all_tasks_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = projects.id
|
||||
AND status_id IN (SELECT id
|
||||
FROM task_statuses
|
||||
WHERE project_id = projects.id
|
||||
AND category_id IN
|
||||
(SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS completed_tasks_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM project_members
|
||||
WHERE project_id = projects.id) AS members_count,
|
||||
(SELECT get_project_members(projects.id)) AS names,
|
||||
(SELECT CASE
|
||||
WHEN ((SELECT MAX(updated_at)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = projects.id) >
|
||||
updated_at)
|
||||
THEN (SELECT MAX(updated_at)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = projects.id)
|
||||
ELSE updated_at END) AS updated_at
|
||||
FROM projects
|
||||
WHERE team_id = $1 ${isArchived} ${isFavorites} ${searchQuery}
|
||||
AND is_member_of_project(projects.id
|
||||
, '${req.user?.id}'
|
||||
, $1)
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT $2 OFFSET $3) t) AS data
|
||||
FROM projects
|
||||
WHERE team_id = $1 ${isArchived} ${isFavorites} ${searchQuery}
|
||||
AND is_member_of_project(projects.id
|
||||
, '${req.user?.id}'
|
||||
, $1)) rec;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.team_id || null, size, offset]);
|
||||
const [data] = result.rows;
|
||||
const projects = Array.isArray(data?.projects.data) ? data?.projects.data : [];
|
||||
for (const project of projects) {
|
||||
project.progress = project.all_tasks_count > 0
|
||||
? ((project.completed_tasks_count / project.all_tasks_count) * 100).toFixed(0) : 0;
|
||||
|
||||
}
|
||||
return res.status(200).send(new ServerResponse(true, data?.projects || this.paginatedDatasetDefaultStruct));
|
||||
}
|
||||
|
||||
private static flatString(text: string) {
|
||||
return (text || "").split(" ").map(s => `'${s}'`).join(",");
|
||||
}
|
||||
|
||||
private static getFilterByCategoryWhereClosure(text: string) {
|
||||
return text ? `AND category_id IN (${this.flatString(text)})` : "";
|
||||
}
|
||||
|
||||
private static getFilterByStatusWhereClosure(text: string) {
|
||||
return text ? `AND status_id IN (${this.flatString(text)})` : "";
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {searchQuery, sortField, sortOrder, size, offset} = this.toPaginationOptions(req.query, "name");
|
||||
|
||||
const filterByMember = !req.user?.owner && !req.user?.is_admin ?
|
||||
` AND is_member_of_project(projects.id, '${req.user?.id}', $1) ` : "";
|
||||
|
||||
const isFavorites = req.query.filter === "1" ? ` AND EXISTS(SELECT user_id FROM favorite_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)` : "";
|
||||
const isArchived = req.query.filter === "2"
|
||||
? ` AND EXISTS(SELECT user_id FROM archived_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)`
|
||||
: ` AND NOT EXISTS(SELECT user_id FROM archived_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)`;
|
||||
const categories = this.getFilterByCategoryWhereClosure(req.query.categories as string);
|
||||
const statuses = this.getFilterByStatusWhereClosure(req.query.statuses as string);
|
||||
|
||||
const q = `
|
||||
SELECT ROW_TO_JSON(rec) AS projects
|
||||
FROM (SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT id,
|
||||
name,
|
||||
(SELECT name FROM sys_project_statuses WHERE id = status_id) AS status,
|
||||
(SELECT color_code FROM sys_project_statuses WHERE id = status_id) AS status_color,
|
||||
(SELECT icon FROM sys_project_statuses WHERE id = status_id) AS status_icon,
|
||||
EXISTS(SELECT user_id
|
||||
FROM favorite_projects
|
||||
WHERE user_id = '${req.user?.id}'
|
||||
AND project_id = projects.id) AS favorite,
|
||||
EXISTS(SELECT user_id
|
||||
FROM archived_projects
|
||||
WHERE user_id = '${req.user?.id}'
|
||||
AND project_id = projects.id) AS archived,
|
||||
color_code,
|
||||
start_date,
|
||||
end_date,
|
||||
category_id,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = projects.id) AS all_tasks_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = projects.id
|
||||
AND status_id IN (SELECT id
|
||||
FROM task_statuses
|
||||
WHERE project_id = projects.id
|
||||
AND category_id IN
|
||||
(SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS completed_tasks_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM project_members
|
||||
WHERE project_id = projects.id) AS members_count,
|
||||
(SELECT get_project_members(projects.id)) AS names,
|
||||
(SELECT name FROM clients WHERE id = projects.client_id) AS client_name,
|
||||
(SELECT name FROM users WHERE id = projects.owner_id) AS project_owner,
|
||||
(SELECT name FROM project_categories WHERE id = projects.category_id) AS category_name,
|
||||
(SELECT color_code
|
||||
FROM project_categories
|
||||
WHERE id = projects.category_id) AS category_color,
|
||||
|
||||
((SELECT team_member_id as team_member_id
|
||||
FROM project_members
|
||||
WHERE project_id = projects.id
|
||||
AND project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER'))) AS project_manager_team_member_id,
|
||||
|
||||
(SELECT default_view
|
||||
FROM project_members prm
|
||||
WHERE prm.project_id = projects.id
|
||||
AND team_member_id = '${req.user?.team_member_id}') AS team_member_default_view,
|
||||
|
||||
(SELECT CASE
|
||||
WHEN ((SELECT MAX(updated_at)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = projects.id) >
|
||||
updated_at)
|
||||
THEN (SELECT MAX(updated_at)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = projects.id)
|
||||
ELSE updated_at END) AS updated_at
|
||||
FROM projects
|
||||
WHERE team_id = $1 ${categories} ${statuses} ${isArchived} ${isFavorites} ${filterByMember} ${searchQuery}
|
||||
ORDER BY ${sortField} ${sortOrder}
|
||||
LIMIT $2 OFFSET $3) t) AS data
|
||||
FROM projects
|
||||
WHERE team_id = $1 ${categories} ${statuses} ${isArchived} ${isFavorites} ${filterByMember} ${searchQuery}) rec;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.team_id || null, size, offset]);
|
||||
const [data] = result.rows;
|
||||
|
||||
for (const project of data?.projects.data || []) {
|
||||
project.progress = project.all_tasks_count > 0
|
||||
? ((project.completed_tasks_count / project.all_tasks_count) * 100).toFixed(0) : 0;
|
||||
|
||||
project.updated_at_string = moment(project.updated_at).fromNow();
|
||||
|
||||
project.names = this.createTagList(project?.names);
|
||||
project.names.map((a: any) => a.color_code = getColor(a.name));
|
||||
|
||||
if (project.project_manager_team_member_id) {
|
||||
project.project_manager = {
|
||||
id : project.project_manager_team_member_id
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data?.projects || this.paginatedDatasetDefaultStruct));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getMembersByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {sortField, sortOrder, size, offset} = this.toPaginationOptions(req.query, "name");
|
||||
|
||||
const q = `
|
||||
SELECT ROW_TO_JSON(rec) AS members
|
||||
FROM (SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT project_members.id,
|
||||
team_member_id,
|
||||
(SELECT name
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = tm.id),
|
||||
(SELECT email
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = tm.id) AS email,
|
||||
u.avatar_url,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = project_members.project_id
|
||||
AND id IN (SELECT task_id
|
||||
FROM tasks_assignees
|
||||
WHERE tasks_assignees.project_member_id = project_members.id)) AS all_tasks_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = project_members.project_id
|
||||
AND id IN (SELECT task_id
|
||||
FROM tasks_assignees
|
||||
WHERE tasks_assignees.project_member_id = project_members.id)
|
||||
AND status_id IN (SELECT id
|
||||
FROM task_statuses
|
||||
WHERE category_id = (SELECT id
|
||||
FROM sys_task_status_categories
|
||||
WHERE is_done IS TRUE))) AS completed_tasks_count,
|
||||
EXISTS(SELECT email
|
||||
FROM email_invitations
|
||||
WHERE team_member_id = project_members.team_member_id
|
||||
AND email_invitations.team_id = $2) AS pending_invitation,
|
||||
(SELECT project_access_levels.name
|
||||
FROM project_access_levels
|
||||
WHERE project_access_levels.id = project_members.project_access_level_id) AS access,
|
||||
(SELECT name FROM job_titles WHERE id = tm.job_title_id) AS job_title
|
||||
FROM project_members
|
||||
INNER JOIN team_members tm ON project_members.team_member_id = tm.id
|
||||
LEFT JOIN users u ON tm.user_id = u.id
|
||||
WHERE project_id = $1
|
||||
ORDER BY ${sortField} ${sortOrder}
|
||||
LIMIT $3 OFFSET $4) t) AS data
|
||||
FROM project_members
|
||||
WHERE project_id = $1) rec;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id ?? null, size, offset]);
|
||||
const [data] = result.rows;
|
||||
|
||||
for (const member of data?.members.data || []) {
|
||||
member.progress = member.all_tasks_count > 0
|
||||
? ((member.completed_tasks_count / member.all_tasks_count) * 100).toFixed(0) : 0;
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data?.members || this.paginatedDatasetDefaultStruct));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT projects.id,
|
||||
projects.name,
|
||||
projects.color_code,
|
||||
projects.notes,
|
||||
projects.key,
|
||||
projects.start_date,
|
||||
projects.end_date,
|
||||
projects.status_id,
|
||||
projects.health_id,
|
||||
projects.created_at,
|
||||
projects.updated_at,
|
||||
projects.folder_id,
|
||||
projects.phase_label,
|
||||
projects.category_id,
|
||||
(projects.estimated_man_days) AS man_days,
|
||||
(projects.estimated_working_days) AS working_days,
|
||||
(projects.hours_per_day) AS hours_per_day,
|
||||
(SELECT name FROM project_categories WHERE id = projects.category_id) AS category_name,
|
||||
(SELECT color_code
|
||||
FROM project_categories
|
||||
WHERE id = projects.category_id) AS category_color,
|
||||
(EXISTS(SELECT 1 FROM project_subscribers WHERE project_id = $1 AND user_id = $3)) AS subscribed,
|
||||
(SELECT name FROM users WHERE id = projects.owner_id) AS project_owner,
|
||||
sps.name AS status,
|
||||
sps.color_code AS status_color,
|
||||
sps.icon AS status_icon,
|
||||
(SELECT name FROM clients WHERE id = projects.client_id) AS client_name,
|
||||
|
||||
(SELECT COALESCE(ROW_TO_JSON(pm), '{}'::JSON)
|
||||
FROM (SELECT team_member_id AS id,
|
||||
(SELECT COALESCE(ROW_TO_JSON(pmi), '{}'::JSON)
|
||||
FROM (SELECT name,
|
||||
email,
|
||||
avatar_url
|
||||
FROM team_member_info_view tmiv
|
||||
WHERE tmiv.team_member_id = pm.team_member_id
|
||||
AND tmiv.team_id = (SELECT team_id FROM projects WHERE id = $1)) pmi) AS project_manager_info,
|
||||
EXISTS(SELECT email
|
||||
FROM email_invitations
|
||||
WHERE team_member_id = pm.team_member_id
|
||||
AND email_invitations.team_id = (SELECT team_id
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_id = pm.team_member_id)) AS pending_invitation,
|
||||
(SELECT active FROM team_members WHERE id = pm.team_member_id)
|
||||
FROM project_members pm
|
||||
WHERE project_id = $1
|
||||
AND project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')) pm) AS project_manager
|
||||
FROM projects
|
||||
LEFT JOIN sys_project_statuses sps ON projects.status_id = sps.id
|
||||
WHERE projects.id = $1
|
||||
AND team_id = $2;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id ?? null, req.user?.id ?? null]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (data && data.project_manager) {
|
||||
data.project_manager.name = data.project_manager.project_manager_info.name;
|
||||
data.project_manager.email = data.project_manager.project_manager_info.email;
|
||||
data.project_manager.avatar_url = data.project_manager.project_manager_info.avatar_url;
|
||||
data.project_manager.color_code = getColor(data.project_manager.name);
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions({
|
||||
raisedExceptions: {
|
||||
"PROJECT_EXISTS_ERROR": `Project with "{0}" name already exists. Please choose a different project name.`
|
||||
}
|
||||
})
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT update_project($1) AS project;`;
|
||||
|
||||
const key = req.body.key?.toString().trim().toUpperCase();
|
||||
|
||||
if (!key)
|
||||
return res.status(200).send(new ServerResponse(false, null, "The project key cannot be empty."));
|
||||
|
||||
if (key.length > 5)
|
||||
return res.status(200).send(new ServerResponse(false, null, "The project key length cannot exceed 5 characters."));
|
||||
|
||||
req.body.id = req.params.id;
|
||||
req.body.team_id = req.user?.team_id || null;
|
||||
req.body.user_id = req.user?.id || null;
|
||||
req.body.folder_id = req.body.folder_id || null;
|
||||
req.body.category_id = req.body.category_id || null;
|
||||
req.body.client_name = req.body.client_name?.trim() || null;
|
||||
req.body.project_created_log = LOG_DESCRIPTIONS.PROJECT_UPDATED;
|
||||
req.body.project_member_added_log = LOG_DESCRIPTIONS.PROJECT_MEMBER_ADDED;
|
||||
req.body.project_member_removed_log = LOG_DESCRIPTIONS.PROJECT_MEMBER_REMOVED;
|
||||
req.body.team_member_id = req.body.project_manager ? req.body.project_manager.id : null;
|
||||
|
||||
const result = await db.query(q, [JSON.stringify(req.body)]);
|
||||
const [data] = result.rows;
|
||||
|
||||
this.notifyProjecManagertUpdates(req.params.id, req.user as IPassportSession, req.body.project_manager ? req.body.project_manager.id : null);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.project));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `DELETE
|
||||
FROM projects
|
||||
WHERE id = $1
|
||||
AND team_id = $2`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT (SELECT COUNT(id)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = $1
|
||||
AND status_id IN
|
||||
(SELECT id
|
||||
FROM task_statuses
|
||||
WHERE category_id =
|
||||
(SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS done_task_count,
|
||||
|
||||
(SELECT COUNT(id)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = $1
|
||||
AND status_id IN
|
||||
(SELECT id
|
||||
FROM task_statuses
|
||||
WHERE category_id IN
|
||||
(SELECT id
|
||||
FROM sys_task_status_categories
|
||||
WHERE is_doing IS TRUE
|
||||
OR is_todo IS TRUE))) AS pending_task_count
|
||||
FROM projects
|
||||
WHERE id = $1
|
||||
AND team_id = $2;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
|
||||
const [data] = result.rows;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getOverviewMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {archived} = req.query;
|
||||
const q = `
|
||||
SELECT team_member_id AS id,
|
||||
FALSE AS active,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = $1
|
||||
AND CASE
|
||||
WHEN ($2 IS TRUE) THEN project_id IS NOT NULL
|
||||
ELSE archived IS FALSE END) AS project_task_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
INNER JOIN tasks t ON tasks_assignees.task_id = t.id
|
||||
WHERE CASE
|
||||
WHEN ($2 IS TRUE) THEN t.project_id IS NOT NULL
|
||||
ELSE archived IS FALSE END
|
||||
AND project_member_id = project_members.id) AS task_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
INNER JOIN tasks t ON tasks_assignees.task_id = t.id
|
||||
INNER JOIN task_statuses ts ON t.status_id = ts.id
|
||||
WHERE CASE
|
||||
WHEN ($2 IS TRUE) THEN t.project_id IS NOT NULL
|
||||
ELSE archived IS FALSE END
|
||||
AND project_member_id = project_members.id
|
||||
AND ts.category_id IN
|
||||
(SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE)) AS done_task_count,
|
||||
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
INNER JOIN tasks t ON tasks_assignees.task_id = t.id
|
||||
INNER JOIN task_statuses ts ON t.status_id = ts.id
|
||||
WHERE CASE
|
||||
WHEN ($2 IS TRUE) THEN t.project_id IS NOT NULL
|
||||
ELSE archived IS FALSE END
|
||||
AND project_member_id = project_members.id
|
||||
AND end_date::DATE < CURRENT_DATE::DATE
|
||||
AND t.status_id NOT IN (SELECT id
|
||||
FROM task_statuses
|
||||
WHERE category_id NOT IN
|
||||
(SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))) AS overdue_task_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
INNER JOIN tasks t ON tasks_assignees.task_id = t.id
|
||||
INNER JOIN task_statuses ts ON t.status_id = ts.id
|
||||
WHERE CASE
|
||||
WHEN ($2 IS TRUE) THEN t.project_id IS NOT NULL
|
||||
ELSE archived IS FALSE END
|
||||
AND project_member_id = project_members.id
|
||||
AND ts.category_id IN
|
||||
(SELECT id
|
||||
FROM sys_task_status_categories
|
||||
WHERE is_doing IS TRUE
|
||||
OR is_todo IS TRUE)) AS pending_task_count,
|
||||
(SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id),
|
||||
u.avatar_url,
|
||||
(SELECT team_member_info_view.email
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = tm.id),
|
||||
(SELECT name FROM job_titles WHERE id = tm.job_title_id) AS job_title
|
||||
FROM project_members
|
||||
INNER JOIN team_members tm ON project_members.team_member_id = tm.id
|
||||
LEFT JOIN users u ON tm.user_id = u.id
|
||||
WHERE project_id = $1;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, archived === "true"]);
|
||||
|
||||
for (const item of result.rows) {
|
||||
item.progress =
|
||||
item.task_count > 0
|
||||
? ((item.done_task_count / item.task_count) * 100).toFixed(0)
|
||||
: 0;
|
||||
item.contribution =
|
||||
item.project_task_count > 0
|
||||
? ((item.task_count / item.project_task_count) * 100).toFixed(0)
|
||||
: 0;
|
||||
item.tasks = [];
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getAllTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {searchQuery, size, offset} = this.toPaginationOptions(req.query, ["tasks.name"]);
|
||||
const filterByMember = !req.user?.owner && !req.user?.is_admin ?
|
||||
` AND is_member_of_project(p.id, '${req.user?.id}', $1) ` : "";
|
||||
|
||||
const isDueSoon = req.query.filter == "1";
|
||||
|
||||
const dueSoon = isDueSoon ? "AND tasks.end_date IS NOT NULL" : "";
|
||||
const orderBy = isDueSoon ? "tasks.end_date DESC" : "p.name";
|
||||
const assignedToMe = req.query.filter == "2" ? `
|
||||
AND tasks.id IN (SELECT task_id
|
||||
FROM tasks_assignees
|
||||
WHERE team_member_id = (SELECT id
|
||||
FROM team_members
|
||||
WHERE user_id = '${req.user?.id}'
|
||||
AND team_id = $1))
|
||||
` : "";
|
||||
|
||||
const q = `
|
||||
SELECT ROW_TO_JSON(rec) AS projects
|
||||
FROM (SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT tasks.id,
|
||||
tasks.name,
|
||||
p.team_id,
|
||||
p.name AS project_name,
|
||||
tasks.start_date,
|
||||
tasks.end_date,
|
||||
p.id AS project_id,
|
||||
p.color_code AS project_color,
|
||||
(SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = tasks.status_id)) AS status_color,
|
||||
(SELECT get_task_assignees(tasks.id)) AS names
|
||||
FROM tasks
|
||||
INNER JOIN projects p ON tasks.project_id = p.id
|
||||
WHERE tasks.archived IS FALSE
|
||||
AND p.team_id = $1 ${filterByMember} ${dueSoon} ${searchQuery} ${assignedToMe}
|
||||
ORDER BY ${orderBy}
|
||||
LIMIT $2 OFFSET $3) t) AS data
|
||||
FROM tasks
|
||||
INNER JOIN projects p ON tasks.project_id = p.id
|
||||
WHERE tasks.archived IS FALSE
|
||||
AND p.team_id = $1 ${filterByMember} ${dueSoon} ${searchQuery} ${assignedToMe}) rec;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.team_id || null, size, offset]);
|
||||
const [data] = result.rows;
|
||||
|
||||
for (const project of data?.projects.data || []) {
|
||||
project.progress = project.all_tasks_count > 0
|
||||
? ((project.completed_tasks_count / project.all_tasks_count) * 100).toFixed(0) : 0;
|
||||
|
||||
project.names = this.createTagList(project?.names);
|
||||
project.names.map((a: any) => a.color_code = getColor(a.name));
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.projects || this.paginatedDatasetDefaultStruct));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getAllProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id AS value, name AS text
|
||||
FROM projects
|
||||
WHERE team_id = $1
|
||||
ORDER BY name;`;
|
||||
const result = await db.query(q, [req.user?.team_id || null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows || []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async toggleFavorite(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT toggle_favorite_project($1, $2);`;
|
||||
const result = await db.query(q, [req.user?.id, req.params.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows || []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async toggleArchive(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT toggle_archive_project($1, $2);`;
|
||||
const result = await db.query(q, [req.user?.id, req.params.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows || []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async toggleArchiveAll(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT toggle_archive_all_projects($1);`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows || []));
|
||||
}
|
||||
|
||||
public static async getProjectManager(projectId: string) {
|
||||
const q = `SELECT team_member_id FROM project_members WHERE project_id = $1 AND project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')`;
|
||||
const result = await db.query(q, [projectId]);
|
||||
return result.rows || [];
|
||||
}
|
||||
|
||||
public static async updateExistPhaseColors() {
|
||||
const q = `SELECT id, name FROM project_phases`;
|
||||
const phases = await db.query(q);
|
||||
|
||||
phases.rows.forEach((phase) => {
|
||||
phase.color_code = getColor(phase.name);
|
||||
});
|
||||
|
||||
const body = {
|
||||
phases: phases.rows
|
||||
};
|
||||
|
||||
const q2 = `SELECT update_existing_phase_colors($1)`;
|
||||
await db.query(q2, [JSON.stringify(body)]);
|
||||
|
||||
}
|
||||
|
||||
public static async updateExistSortOrder() {
|
||||
const q = `SELECT id, project_id FROM project_phases ORDER BY name`;
|
||||
const phases = await db.query(q);
|
||||
|
||||
const sortNumbers: any = {};
|
||||
|
||||
phases.rows.forEach(phase => {
|
||||
const projectId = phase.project_id;
|
||||
|
||||
if (!sortNumbers[projectId]) {
|
||||
sortNumbers[projectId] = 0;
|
||||
}
|
||||
|
||||
phase.sort_number = sortNumbers[projectId]++;
|
||||
});
|
||||
|
||||
const body = {
|
||||
phases: phases.rows
|
||||
};
|
||||
|
||||
const q2 = `SELECT update_existing_phase_sort_order($1)`;
|
||||
await db.query(q2, [JSON.stringify(body)]);
|
||||
// return phases;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
1844
worklenz-backend/src/controllers/reporting-controller.ts
Normal file
1844
worklenz-backend/src/controllers/reporting-controller.ts
Normal file
File diff suppressed because it is too large
Load Diff
60
worklenz-backend/src/controllers/reporting/interfaces.ts
Normal file
60
worklenz-backend/src/controllers/reporting/interfaces.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { IChartObject } from "./overview/reporting-overview-base";
|
||||
|
||||
export interface IDuration {
|
||||
label: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface IReportingInfo {
|
||||
organization_name: string;
|
||||
}
|
||||
|
||||
export interface ITeamStatistics {
|
||||
count: number;
|
||||
projects: number;
|
||||
members: number;
|
||||
}
|
||||
|
||||
export interface IProjectStatistics {
|
||||
count: number;
|
||||
active: number;
|
||||
overdue: number;
|
||||
}
|
||||
|
||||
export interface IMemberStatistics {
|
||||
count: number;
|
||||
unassigned: number;
|
||||
overdue: number;
|
||||
}
|
||||
|
||||
export interface IOverviewStatistics {
|
||||
teams: ITeamStatistics;
|
||||
projects: IProjectStatistics;
|
||||
members: IMemberStatistics;
|
||||
}
|
||||
|
||||
export interface IChartData {
|
||||
chart: IChartObject[];
|
||||
}
|
||||
|
||||
export interface ITasksByStatus extends IChartData {
|
||||
all: number;
|
||||
todo: number;
|
||||
doing: number;
|
||||
done: number;
|
||||
}
|
||||
|
||||
export interface ITasksByPriority extends IChartData {
|
||||
all: number;
|
||||
low: number;
|
||||
medium: number;
|
||||
high: number;
|
||||
}
|
||||
|
||||
export interface ITasksByDue extends IChartData {
|
||||
all: number;
|
||||
completed: number;
|
||||
upcoming: number;
|
||||
overdue: number;
|
||||
no_due: number;
|
||||
}
|
||||
@@ -0,0 +1,993 @@
|
||||
import db from "../../../config/db";
|
||||
import { ITasksByDue, ITasksByPriority, ITasksByStatus } from "../interfaces";
|
||||
import ReportingControllerBase from "../reporting-controller-base";
|
||||
import {
|
||||
DATE_RANGES,
|
||||
TASK_DUE_COMPLETED_COLOR,
|
||||
TASK_DUE_NO_DUE_COLOR,
|
||||
TASK_DUE_OVERDUE_COLOR,
|
||||
TASK_DUE_UPCOMING_COLOR,
|
||||
TASK_PRIORITY_HIGH_COLOR,
|
||||
TASK_PRIORITY_LOW_COLOR,
|
||||
TASK_PRIORITY_MEDIUM_COLOR,
|
||||
TASK_STATUS_DOING_COLOR,
|
||||
TASK_STATUS_DONE_COLOR,
|
||||
TASK_STATUS_TODO_COLOR
|
||||
} from "../../../shared/constants";
|
||||
import { formatDuration, int } from "../../../shared/utils";
|
||||
import moment from "moment";
|
||||
|
||||
export interface IChartObject {
|
||||
name: string,
|
||||
color: string,
|
||||
y: number
|
||||
}
|
||||
|
||||
export default class ReportingOverviewBase extends ReportingControllerBase {
|
||||
|
||||
private static createChartObject(name: string, color: string, y: number) {
|
||||
return {
|
||||
name,
|
||||
color,
|
||||
y
|
||||
};
|
||||
}
|
||||
|
||||
protected static async getTeamsCounts(teamId: string | null, archivedQuery = "") {
|
||||
|
||||
const q = `
|
||||
SELECT JSON_BUILD_OBJECT(
|
||||
'teams', (SELECT COUNT(*) FROM teams WHERE in_organization(id, $1)),
|
||||
'projects',
|
||||
(SELECT COUNT(*) FROM projects WHERE in_organization(team_id, $1) ${archivedQuery}),
|
||||
'team_members', (SELECT COUNT(DISTINCT email)
|
||||
FROM team_member_info_view
|
||||
WHERE in_organization(team_id, $1))
|
||||
) AS counts;
|
||||
`;
|
||||
|
||||
const res = await db.query(q, [teamId]);
|
||||
const [data] = res.rows;
|
||||
|
||||
return {
|
||||
count: int(data?.counts.teams),
|
||||
projects: int(data?.counts.projects),
|
||||
members: int(data?.counts.team_members),
|
||||
};
|
||||
}
|
||||
|
||||
protected static async getProjectsCounts(teamId: string | null, archivedQuery = "") {
|
||||
const q = `
|
||||
SELECT JSON_BUILD_OBJECT(
|
||||
'active_projects', (SELECT COUNT(*)
|
||||
FROM projects
|
||||
WHERE in_organization(team_id, $1) AND (end_date > CURRENT_TIMESTAMP
|
||||
OR end_date IS NULL) ${archivedQuery}),
|
||||
'overdue_projects', (SELECT COUNT(*)
|
||||
FROM projects
|
||||
WHERE in_organization(team_id, $1)
|
||||
AND end_date < CURRENT_TIMESTAMP
|
||||
AND status_id NOT IN
|
||||
(SELECT id FROM sys_project_statuses WHERE name = 'Completed') ${archivedQuery})
|
||||
) AS counts;
|
||||
`;
|
||||
|
||||
const res = await db.query(q, [teamId]);
|
||||
const [data] = res.rows;
|
||||
|
||||
return {
|
||||
count: 0,
|
||||
active: int(data?.counts.active_projects),
|
||||
overdue: int(data?.counts.overdue_projects),
|
||||
};
|
||||
}
|
||||
|
||||
protected static async getMemberCounts(teamId: string | null) {
|
||||
const q = `
|
||||
SELECT JSON_BUILD_OBJECT(
|
||||
'unassigned', (SELECT COUNT(*)
|
||||
FROM team_members
|
||||
WHERE in_organization(team_id, $1)
|
||||
AND id NOT IN (SELECT team_member_id FROM tasks_assignees)),
|
||||
'with_overdue', (SELECT COUNT(*)
|
||||
FROM team_members
|
||||
WHERE in_organization(team_id, $1)
|
||||
AND id IN (SELECT team_member_id
|
||||
FROM tasks_assignees
|
||||
WHERE is_overdue(task_id) IS TRUE))
|
||||
) AS counts;
|
||||
`;
|
||||
|
||||
const res = await db.query(q, [teamId]);
|
||||
const [data] = res.rows;
|
||||
|
||||
return {
|
||||
count: 0,
|
||||
unassigned: int(data?.counts.unassigned),
|
||||
overdue: int(data?.counts.with_overdue),
|
||||
};
|
||||
}
|
||||
|
||||
protected static async getProjectStats(projectId: string | null) {
|
||||
const q = `
|
||||
SELECT JSON_BUILD_OBJECT(
|
||||
'completed', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND is_completed(tasks.status_id, tasks.project_id) IS TRUE),
|
||||
'incompleted', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND is_completed(tasks.status_id, tasks.project_id) IS FALSE),
|
||||
'overdue', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND is_overdue(tasks.id)),
|
||||
'total_allocated', (SELECT SUM(total_minutes)
|
||||
FROM tasks
|
||||
WHERE project_id = $1),
|
||||
'total_logged', (SELECT SUM((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = tasks.id))
|
||||
FROM tasks
|
||||
WHERE project_id = $1)
|
||||
) AS counts;
|
||||
`;
|
||||
|
||||
const res = await db.query(q, [projectId]);
|
||||
const [data] = res.rows;
|
||||
|
||||
return {
|
||||
completed: int(data?.counts.completed),
|
||||
incompleted: int(data?.counts.incompleted),
|
||||
overdue: int(data?.counts.overdue),
|
||||
total_allocated: moment.duration(int(data?.counts.total_allocated), "minutes").asHours().toFixed(0),
|
||||
total_logged: moment.duration(int(data?.counts.total_logged), "seconds").asHours().toFixed(0),
|
||||
};
|
||||
}
|
||||
|
||||
protected static async getTasksByStatus(projectId: string | null): Promise<ITasksByStatus> {
|
||||
const q = `
|
||||
SELECT JSON_BUILD_OBJECT(
|
||||
'all', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1),
|
||||
'todo', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND is_todo(tasks.status_id, tasks.project_id) IS TRUE),
|
||||
'doing', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND is_doing(tasks.status_id, tasks.project_id) IS TRUE),
|
||||
'done', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND is_completed(tasks.status_id, tasks.project_id) IS TRUE)
|
||||
) AS counts;
|
||||
`;
|
||||
|
||||
const res = await db.query(q, [projectId]);
|
||||
const [data] = res.rows;
|
||||
|
||||
const all = int(data?.counts.all);
|
||||
const todo = int(data?.counts.todo);
|
||||
const doing = int(data?.counts.doing);
|
||||
const done = int(data?.counts.done);
|
||||
|
||||
const chart: IChartObject[] = [];
|
||||
|
||||
return {
|
||||
all,
|
||||
todo,
|
||||
doing,
|
||||
done,
|
||||
chart
|
||||
};
|
||||
}
|
||||
|
||||
protected static async getTasksByPriority(projectId: string | null): Promise<ITasksByPriority> {
|
||||
const q = `
|
||||
SELECT JSON_BUILD_OBJECT(
|
||||
'low', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND priority_id = (SELECT id FROM task_priorities WHERE value = 0)),
|
||||
'medium', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND priority_id = (SELECT id FROM task_priorities WHERE value = 1)),
|
||||
'high', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND priority_id = (SELECT id FROM task_priorities WHERE value = 2))
|
||||
) AS counts;
|
||||
`;
|
||||
|
||||
const res = await db.query(q, [projectId]);
|
||||
const [data] = res.rows;
|
||||
|
||||
const low = int(data?.counts.low);
|
||||
const medium = int(data?.counts.medium);
|
||||
const high = int(data?.counts.high);
|
||||
|
||||
const chart: IChartObject[] = [];
|
||||
|
||||
return {
|
||||
all: 0,
|
||||
low,
|
||||
medium,
|
||||
high,
|
||||
chart
|
||||
};
|
||||
}
|
||||
|
||||
protected static async getTaskCountsByDue(projectId: string | null): Promise<ITasksByDue> {
|
||||
const q = `
|
||||
SELECT JSON_BUILD_OBJECT(
|
||||
'no_due', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND end_date IS NULL),
|
||||
'upcoming', (SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
AND end_date > CURRENT_TIMESTAMP)
|
||||
) AS counts;
|
||||
`;
|
||||
|
||||
const res = await db.query(q, [projectId]);
|
||||
const [data] = res.rows;
|
||||
|
||||
const chart: IChartObject[] = [];
|
||||
|
||||
return {
|
||||
all: 0,
|
||||
completed: 0,
|
||||
upcoming: int(data?.counts.upcoming),
|
||||
overdue: 0,
|
||||
no_due: int(data?.counts.no_due),
|
||||
chart
|
||||
};
|
||||
}
|
||||
|
||||
protected static createByStatusChartData(body: ITasksByStatus) {
|
||||
body.chart = [
|
||||
this.createChartObject("Todo", TASK_STATUS_TODO_COLOR, body.todo),
|
||||
this.createChartObject("Doing", TASK_STATUS_DOING_COLOR, body.doing),
|
||||
this.createChartObject("Done", TASK_STATUS_DONE_COLOR, body.done),
|
||||
];
|
||||
}
|
||||
|
||||
protected static createByPriorityChartData(body: ITasksByPriority) {
|
||||
body.chart = [
|
||||
this.createChartObject("Low", TASK_PRIORITY_LOW_COLOR, body.low),
|
||||
this.createChartObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, body.medium),
|
||||
this.createChartObject("High", TASK_PRIORITY_HIGH_COLOR, body.high),
|
||||
];
|
||||
}
|
||||
|
||||
protected static createByDueDateChartData(body: ITasksByDue) {
|
||||
body.chart = [
|
||||
this.createChartObject("Completed", TASK_DUE_COMPLETED_COLOR, body.completed),
|
||||
this.createChartObject("Upcoming", TASK_DUE_UPCOMING_COLOR, body.upcoming),
|
||||
this.createChartObject("Overdue", TASK_DUE_OVERDUE_COLOR, body.overdue),
|
||||
this.createChartObject("No due date", TASK_DUE_NO_DUE_COLOR, body.no_due),
|
||||
];
|
||||
}
|
||||
|
||||
// Team Member Overview
|
||||
|
||||
protected static async getProjectCountOfTeamMember(teamMemberId: string | null, includeArchived: boolean, userId: string) {
|
||||
|
||||
const archivedClause = includeArchived
|
||||
? ""
|
||||
: `AND pm.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = pm.project_id AND archived_projects.user_id = '${userId}')`;
|
||||
|
||||
|
||||
const q = `
|
||||
SELECT COUNT(*)
|
||||
FROM project_members pm
|
||||
WHERE team_member_id = $1 ${archivedClause};
|
||||
`;
|
||||
const result = await db.query(q, [teamMemberId]);
|
||||
const [data] = result.rows;
|
||||
return int(data.count);
|
||||
}
|
||||
|
||||
protected static async getTeamCountOfTeamMember(teamMemberId: string | null) {
|
||||
const q = `
|
||||
SELECT COUNT(*)
|
||||
FROM team_members
|
||||
WHERE id = $1;
|
||||
`;
|
||||
const result = await db.query(q, [teamMemberId]);
|
||||
const [data] = result.rows;
|
||||
return int(data.count);
|
||||
}
|
||||
|
||||
protected static memberTasksDurationFilter(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 `AND t.end_date::DATE >= '${start}'::DATE AND t.end_date::DATE <= '${end}'::DATE`;
|
||||
}
|
||||
|
||||
if (key === DATE_RANGES.YESTERDAY)
|
||||
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE`;
|
||||
if (key === DATE_RANGES.LAST_WEEK)
|
||||
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
||||
if (key === DATE_RANGES.LAST_MONTH)
|
||||
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
||||
if (key === DATE_RANGES.LAST_QUARTER)
|
||||
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
||||
|
||||
return "";
|
||||
|
||||
}
|
||||
|
||||
protected static activityLogDurationFilter(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 `
|
||||
AND (is_doing(
|
||||
(SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' AND tl.created_at::DATE <= '${end}'::DATE ORDER BY tl.created_at DESC LIMIT 1
|
||||
)::UUID, t.project_id)
|
||||
OR is_todo(
|
||||
(SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' AND tl.created_at::DATE <= '${end}'::DATE ORDER BY tl.created_at DESC LIMIT 1
|
||||
)::UUID, t.project_id)
|
||||
OR is_completed_between(t.id::UUID, '${start}'::DATE, '${end}'::DATE))`;
|
||||
}
|
||||
return `AND (is_doing(
|
||||
(SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' AND tl.created_at::DATE <= NOW()::DATE ORDER BY tl.created_at DESC LIMIT 1
|
||||
)::UUID, t.project_id)
|
||||
OR is_todo(
|
||||
(SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' AND tl.created_at::DATE <= NOW()::DATE ORDER BY tl.created_at DESC LIMIT 1
|
||||
)::UUID, t.project_id)
|
||||
OR is_completed(t.status_id::UUID, t.project_id::UUID))`;
|
||||
}
|
||||
|
||||
protected static memberAssignDurationFilter(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");
|
||||
|
||||
if (start === end) {
|
||||
return `AND ta.updated_at::DATE = '${start}'::DATE`;
|
||||
}
|
||||
|
||||
return `AND ta.updated_at::DATE >= '${start}'::DATE AND ta.updated_at::DATE <= '${end}'::DATE`;
|
||||
}
|
||||
|
||||
if (key === DATE_RANGES.YESTERDAY)
|
||||
return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE`;
|
||||
if (key === DATE_RANGES.LAST_WEEK)
|
||||
return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
||||
if (key === DATE_RANGES.LAST_MONTH)
|
||||
return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
||||
if (key === DATE_RANGES.LAST_QUARTER)
|
||||
return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
protected static completedDurationFilter(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");
|
||||
|
||||
if (start === end) {
|
||||
return `AND t.completed_at::DATE = '${start}'::DATE`;
|
||||
}
|
||||
|
||||
return `AND t.completed_at::DATE >= '${start}'::DATE AND t.completed_at::DATE <= '${end}'::DATE`;
|
||||
}
|
||||
|
||||
if (key === DATE_RANGES.YESTERDAY)
|
||||
return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE`;
|
||||
if (key === DATE_RANGES.LAST_WEEK)
|
||||
return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
||||
if (key === DATE_RANGES.LAST_MONTH)
|
||||
return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
||||
if (key === DATE_RANGES.LAST_QUARTER)
|
||||
return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
protected static overdueTasksByDate(key: string, dateRange: string[], archivedClause: string) {
|
||||
if (dateRange.length === 2) {
|
||||
const end = moment(dateRange[1]).format("YYYY-MM-DD");
|
||||
return `(SELECT COUNT(CASE WHEN is_overdue_for_date(t.id, '${end}'::DATE) IS TRUE THEN 1 END)
|
||||
FROM tasks t
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 ${archivedClause})`;
|
||||
}
|
||||
|
||||
return `(SELECT COUNT(CASE WHEN is_overdue_for_date(t.id, NOW()::DATE) IS TRUE THEN 1 END)
|
||||
FROM tasks t
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 ${archivedClause})`;
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected static overdueTasksDurationFilter(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 `AND t.end_date::DATE >= '${start}'::DATE AND t.end_date::DATE <= '${end}'::DATE`;
|
||||
}
|
||||
|
||||
if (key === DATE_RANGES.YESTERDAY)
|
||||
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND t.end_date::DATE < NOW()::DATE`;
|
||||
if (key === DATE_RANGES.LAST_WEEK)
|
||||
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND t.end_date::DATE < NOW()::DATE`;
|
||||
if (key === DATE_RANGES.LAST_MONTH)
|
||||
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND t.end_date::DATE < NOW()::DATE`;
|
||||
if (key === DATE_RANGES.LAST_QUARTER)
|
||||
return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND t.end_date::DATE < NOW()::DATE`;
|
||||
if (key === DATE_RANGES.ALL_TIME)
|
||||
return `AND t.end_date::DATE < NOW()::DATE`;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
protected static taskWorklogDurationFilter(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 `AND created_at::DATE >= '${start}'::DATE AND created_at::DATE <= '${end}'::DATE`;
|
||||
}
|
||||
|
||||
if (key === DATE_RANGES.YESTERDAY)
|
||||
return `AND created_at::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND created_at::DATE < CURRENT_DATE::DATE`;
|
||||
if (key === DATE_RANGES.LAST_WEEK)
|
||||
return `AND created_at::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND created_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
||||
if (key === DATE_RANGES.LAST_MONTH)
|
||||
return `AND created_at::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND created_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
||||
if (key === DATE_RANGES.LAST_QUARTER)
|
||||
return `AND created_at::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND created_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
protected static async getTeamMemberStats(teamMemberId: string | null, includeArchived: boolean, userId: string) {
|
||||
|
||||
const archivedClause = includeArchived
|
||||
? ""
|
||||
: `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`;
|
||||
|
||||
const q = `SELECT JSON_BUILD_OBJECT(
|
||||
|
||||
'total_tasks', (SELECT COUNT(ta.task_id)
|
||||
FROM tasks t
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 ${archivedClause}),
|
||||
|
||||
'completed', (SELECT COUNT(CASE WHEN is_completed(t.status_id, t.project_id) IS TRUE THEN 1 END)
|
||||
FROM tasks t
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 ${archivedClause}),
|
||||
|
||||
'ongoing', (SELECT COUNT(CASE WHEN is_doing(t.status_id, t.project_id) IS TRUE THEN 1 END)
|
||||
FROM tasks t
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 ${archivedClause}),
|
||||
|
||||
'overdue', (SELECT COUNT(CASE WHEN is_overdue(t.id) IS TRUE THEN 1 END)
|
||||
FROM tasks t
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 ${archivedClause}),
|
||||
|
||||
'total_logged', (SELECT SUM((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id AND user_id = (SELECT user_id FROM team_member_info_view WHERE team_member_info_view.team_member_id = $1) ${archivedClause})) AS total_logged
|
||||
|
||||
FROM tasks t
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1)
|
||||
) AS counts;`;
|
||||
|
||||
const res = await db.query(q, [teamMemberId]);
|
||||
const [data] = res.rows;
|
||||
|
||||
return {
|
||||
teams: 0,
|
||||
projects: 0,
|
||||
completed: int(data?.counts.completed),
|
||||
ongoing: int(data?.counts.ongoing),
|
||||
overdue: int(data?.counts.overdue),
|
||||
total_tasks: int(data?.counts.total_tasks),
|
||||
total_logged: formatDuration(moment.duration(data?.counts.total_logged, "seconds")),
|
||||
};
|
||||
}
|
||||
|
||||
protected static async getMemberStats(teamMemberId: string | null, key: string, dateRange: string[] | [], includeArchived: boolean, userId: string) {
|
||||
|
||||
const archivedClause = includeArchived
|
||||
? ""
|
||||
: `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`;
|
||||
|
||||
const durationFilter = this.memberTasksDurationFilter(key, dateRange);
|
||||
const workLogDurationFilter = this.taskWorklogDurationFilter(key, dateRange);
|
||||
const assignClause = this.memberAssignDurationFilter(key, dateRange);
|
||||
const completedDurationClasue = this.completedDurationFilter(key, dateRange);
|
||||
const overdueClauseByDate = this.overdueTasksByDate(key, dateRange, archivedClause);
|
||||
|
||||
const q = `SELECT JSON_BUILD_OBJECT(
|
||||
|
||||
'total_tasks', (SELECT COUNT(ta.task_id)
|
||||
FROM tasks t
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 ${durationFilter} ${archivedClause}),
|
||||
|
||||
'assigned', (SELECT COUNT(ta.task_id)
|
||||
FROM tasks t
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 ${assignClause} ${archivedClause}),
|
||||
|
||||
'completed', (SELECT COUNT(CASE WHEN is_completed(t.status_id, t.project_id) IS TRUE THEN 1 END)
|
||||
FROM tasks t
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 ${completedDurationClasue} ${archivedClause}),
|
||||
|
||||
'ongoing', (SELECT COUNT(CASE WHEN is_doing(t.status_id, t.project_id) IS TRUE THEN 1 END)
|
||||
FROM tasks t
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 ${archivedClause}),
|
||||
|
||||
'overdue', ${overdueClauseByDate},
|
||||
|
||||
'total_logged', (SELECT SUM((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id AND user_id = (SELECT user_id FROM team_member_info_view WHERE team_member_info_view.team_member_id = $1) ${workLogDurationFilter} ${archivedClause})) AS total_logged
|
||||
|
||||
FROM tasks t
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1)
|
||||
) AS counts;`;
|
||||
|
||||
const res = await db.query(q, [teamMemberId]);
|
||||
const [data] = res.rows;
|
||||
|
||||
return {
|
||||
teams: 0,
|
||||
projects: 0,
|
||||
assigned: int(data?.counts.assigned),
|
||||
completed: int(data?.counts.completed),
|
||||
ongoing: int(data?.counts.ongoing),
|
||||
overdue: int(data?.counts.overdue),
|
||||
total_tasks: int(data?.counts.total_tasks),
|
||||
total_logged: formatDuration(moment.duration(data?.counts.total_logged, "seconds")),
|
||||
};
|
||||
}
|
||||
|
||||
protected static async getTasksByProjectOfTeamMemberOverview(teamMemberId: string | null, includeArchived: boolean, userId: string) {
|
||||
|
||||
const archivedClause = includeArchived
|
||||
? ""
|
||||
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND archived_projects.user_id = '${userId}')`;
|
||||
|
||||
const q = `
|
||||
SELECT p.id,
|
||||
p.color_code AS color,
|
||||
p.name AS label,
|
||||
COUNT(t.id) AS count
|
||||
FROM projects p
|
||||
JOIN tasks t ON p.id = t.project_id
|
||||
JOIN tasks_assignees ta ON t.id = ta.task_id AND ta.team_member_id = $1
|
||||
JOIN project_members pm ON p.id = pm.project_id AND pm.team_member_id = $1
|
||||
WHERE (is_doing(t.status_id, t.project_id)
|
||||
OR is_todo(t.status_id, t.project_id)
|
||||
OR is_completed(t.status_id, t.project_id)) ${archivedClause}
|
||||
GROUP BY p.id, p.name;
|
||||
`;
|
||||
const result = await db.query(q, [teamMemberId]);
|
||||
|
||||
const chart: IChartObject[] = [];
|
||||
|
||||
const total = result.rows.reduce((accumulator: number, current: {
|
||||
count: number
|
||||
}) => accumulator + int(current.count), 0);
|
||||
|
||||
for (const project of result.rows) {
|
||||
project.count = int(project.count);
|
||||
chart.push(this.createChartObject(project.label, project.color, project.count));
|
||||
}
|
||||
|
||||
return { chart, total, data: result.rows };
|
||||
}
|
||||
|
||||
protected static async getTasksByProjectOfTeamMember(teamMemberId: string | null, key: string, dateRange: string[] | [], includeArchived: boolean, userId: string) {
|
||||
|
||||
const archivedClause = includeArchived
|
||||
? ""
|
||||
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND archived_projects.user_id = '${userId}')`;
|
||||
|
||||
const durationFilter = this.memberTasksDurationFilter(key, dateRange);
|
||||
const activityLogDateFilter = this.getActivityLogsCreationClause(key, dateRange);
|
||||
const completedDatebetweenClause = this.getCompletedBetweenClause(key, dateRange);
|
||||
|
||||
const q = `
|
||||
SELECT p.id,
|
||||
p.color_code AS color,
|
||||
p.name AS label,
|
||||
COUNT(t.id) AS count
|
||||
FROM projects p
|
||||
JOIN tasks t ON p.id = t.project_id
|
||||
JOIN tasks_assignees ta ON t.id = ta.task_id AND ta.team_member_id = $1
|
||||
JOIN project_members pm ON p.id = pm.project_id AND pm.team_member_id = $1
|
||||
WHERE (is_doing(
|
||||
(SELECT new_value
|
||||
FROM task_activity_logs tl
|
||||
WHERE tl.task_id = t.id
|
||||
AND tl.attribute_type = 'status'
|
||||
${activityLogDateFilter}
|
||||
ORDER BY tl.created_at DESC
|
||||
LIMIT 1)::UUID, t.project_id)
|
||||
OR is_todo(
|
||||
(SELECT new_value
|
||||
FROM task_activity_logs tl
|
||||
WHERE tl.task_id = t.id
|
||||
AND tl.attribute_type = 'status'
|
||||
${activityLogDateFilter}
|
||||
ORDER BY tl.created_at DESC
|
||||
LIMIT 1)::UUID, t.project_id)
|
||||
OR ${completedDatebetweenClause}) ${archivedClause}
|
||||
GROUP BY p.id, p.name;
|
||||
`;
|
||||
const result = await db.query(q, [teamMemberId]);
|
||||
|
||||
const chart: IChartObject[] = [];
|
||||
|
||||
const total = result.rows.reduce((accumulator: number, current: {
|
||||
count: number
|
||||
}) => accumulator + int(current.count), 0);
|
||||
|
||||
for (const project of result.rows) {
|
||||
project.count = int(project.count);
|
||||
chart.push(this.createChartObject(project.label, project.color, project.count));
|
||||
}
|
||||
|
||||
return { chart, total, data: result.rows };
|
||||
}
|
||||
|
||||
protected static async getTasksByPriorityOfTeamMemberOverview(teamMemberId: string | null, includeArchived: boolean, userId: string) {
|
||||
|
||||
const archivedClause = includeArchived
|
||||
? ""
|
||||
: `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`;
|
||||
|
||||
|
||||
const q = `
|
||||
SELECT COUNT(CASE WHEN tp.value = 0 THEN 1 END) AS low,
|
||||
COUNT(CASE WHEN tp.value = 1 THEN 1 END) AS medium,
|
||||
COUNT(CASE WHEN tp.value = 2 THEN 1 END) AS high
|
||||
FROM tasks t
|
||||
LEFT JOIN task_priorities tp ON t.priority_id = tp.id
|
||||
JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 AND (is_doing(t.status_id, t.project_id)
|
||||
OR is_todo(t.status_id, t.project_id)
|
||||
OR is_completed(t.status_id, t.project_id)) ${archivedClause};
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [teamMemberId]);
|
||||
const [d] = result.rows;
|
||||
|
||||
const total = int(d.low) + int(d.medium) + int(d.high);
|
||||
|
||||
const chart = [
|
||||
this.createChartObject("Low", TASK_PRIORITY_LOW_COLOR, d.low),
|
||||
this.createChartObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, d.medium),
|
||||
this.createChartObject("High", TASK_PRIORITY_HIGH_COLOR, d.high),
|
||||
];
|
||||
|
||||
const data = [
|
||||
{ label: "Low", color: TASK_PRIORITY_LOW_COLOR, count: d.low },
|
||||
{ label: "Medium", color: TASK_PRIORITY_MEDIUM_COLOR, count: d.medium },
|
||||
{ label: "High", color: TASK_PRIORITY_HIGH_COLOR, count: d.high },
|
||||
];
|
||||
|
||||
return { chart, total, data };
|
||||
}
|
||||
|
||||
protected static async getTasksByPriorityOfTeamMember(teamMemberId: string | null, key: string, dateRange: string[] | [], includeArchived: boolean, userId: string) {
|
||||
|
||||
const archivedClause = includeArchived
|
||||
? ""
|
||||
: `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`;
|
||||
|
||||
|
||||
const durationFilter = this.memberTasksDurationFilter(key, dateRange);
|
||||
const activityLogDateFilter = this.getActivityLogsCreationClause(key, dateRange);
|
||||
const completedDatebetweenClause = this.getCompletedBetweenClause(key, dateRange);
|
||||
|
||||
const q = `
|
||||
SELECT COUNT(CASE WHEN tp.value = 0 THEN 1 END) AS low,
|
||||
COUNT(CASE WHEN tp.value = 1 THEN 1 END) AS medium,
|
||||
COUNT(CASE WHEN tp.value = 2 THEN 1 END) AS high
|
||||
FROM tasks t
|
||||
LEFT JOIN task_priorities tp ON t.priority_id = tp.id
|
||||
JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 AND (is_doing(
|
||||
(SELECT new_value
|
||||
FROM task_activity_logs tl
|
||||
WHERE tl.task_id = t.id
|
||||
AND tl.attribute_type = 'status'
|
||||
${activityLogDateFilter}
|
||||
ORDER BY tl.created_at DESC
|
||||
LIMIT 1)::UUID, t.project_id)
|
||||
OR is_todo(
|
||||
(SELECT new_value
|
||||
FROM task_activity_logs tl
|
||||
WHERE tl.task_id = t.id
|
||||
AND tl.attribute_type = 'status'
|
||||
${activityLogDateFilter}
|
||||
ORDER BY tl.created_at DESC
|
||||
LIMIT 1)::UUID, t.project_id)
|
||||
OR ${completedDatebetweenClause}) ${archivedClause};
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [teamMemberId]);
|
||||
const [d] = result.rows;
|
||||
|
||||
const total = int(d.low) + int(d.medium) + int(d.high);
|
||||
|
||||
const chart = [
|
||||
this.createChartObject("Low", TASK_PRIORITY_LOW_COLOR, d.low),
|
||||
this.createChartObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, d.medium),
|
||||
this.createChartObject("High", TASK_PRIORITY_HIGH_COLOR, d.high),
|
||||
];
|
||||
|
||||
const data = [
|
||||
{ label: "Low", color: TASK_PRIORITY_LOW_COLOR, count: d.low },
|
||||
{ label: "Medium", color: TASK_PRIORITY_MEDIUM_COLOR, count: d.medium },
|
||||
{ label: "High", color: TASK_PRIORITY_HIGH_COLOR, count: d.high },
|
||||
];
|
||||
|
||||
return { chart, total, data };
|
||||
}
|
||||
|
||||
protected static getActivityLogsCreationClause(key: string, dateRange: string[]) {
|
||||
if (dateRange.length === 2) {
|
||||
const end = moment(dateRange[1]).format("YYYY-MM-DD");
|
||||
return `AND tl.created_at::DATE <= '${end}'::DATE`;
|
||||
}
|
||||
return `AND tl.created_at::DATE <= NOW()::DATE`;
|
||||
}
|
||||
|
||||
protected static getCompletedBetweenClause(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 `is_completed_between(t.id::UUID, '${start}'::DATE, '${end}'::DATE)`;
|
||||
}
|
||||
return `is_completed(t.status_id::UUID, t.project_id::UUID)`;
|
||||
}
|
||||
|
||||
protected static async getTasksByStatusOfTeamMemberOverview(teamMemberId: string | null, includeArchived: boolean, userId: string) {
|
||||
|
||||
const archivedClause = includeArchived
|
||||
? ""
|
||||
: `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`;
|
||||
|
||||
|
||||
const q = `
|
||||
SELECT COUNT(ta.task_id) AS total,
|
||||
COUNT(CASE WHEN is_todo(t.status_id, t.project_id) IS TRUE THEN 1 END) AS todo,
|
||||
COUNT(CASE WHEN is_doing(t.status_id, t.project_id) IS TRUE THEN 1 END) AS doing,
|
||||
COUNT(CASE WHEN is_completed(t.status_id, t.project_id) IS TRUE THEN 1 END) AS done
|
||||
FROM tasks t
|
||||
JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 ${archivedClause};
|
||||
`;
|
||||
|
||||
const res = await db.query(q, [teamMemberId]);
|
||||
const [d] = res.rows;
|
||||
|
||||
const total = int(d.total);
|
||||
|
||||
const chart = [
|
||||
this.createChartObject("Todo", TASK_STATUS_TODO_COLOR, d.todo),
|
||||
this.createChartObject("Doing", TASK_STATUS_DOING_COLOR, d.doing),
|
||||
this.createChartObject("Done", TASK_STATUS_DONE_COLOR, d.done),
|
||||
];
|
||||
|
||||
const data = [
|
||||
{ label: "Todo", color: TASK_STATUS_TODO_COLOR, count: d.todo },
|
||||
{ label: "Doing", color: TASK_STATUS_DOING_COLOR, count: d.doing },
|
||||
{ label: "Done", color: TASK_STATUS_DONE_COLOR, count: d.done },
|
||||
];
|
||||
|
||||
|
||||
return { chart, total, data };
|
||||
}
|
||||
|
||||
protected static async getTasksByStatusOfTeamMember(teamMemberId: string | null, key: string, dateRange: string[] | [], includeArchived: boolean, userId: string) {
|
||||
|
||||
const archivedClause = includeArchived
|
||||
? ""
|
||||
: `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`;
|
||||
|
||||
|
||||
const durationFilter = this.memberTasksDurationFilter(key, dateRange);
|
||||
const completedBetweenFilter = this.getCompletedBetweenClause(key, dateRange);
|
||||
const activityLogCreationFilter = this.getActivityLogsCreationClause(key, dateRange);
|
||||
|
||||
const q = `
|
||||
SELECT COUNT(ta.task_id) AS total,
|
||||
COUNT(CASE WHEN is_todo((SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' ${activityLogCreationFilter} ORDER BY tl.created_at DESC LIMIT 1)::UUID, t.project_id) IS TRUE THEN 1 END) AS todo,
|
||||
COUNT(CASE WHEN is_doing((SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' ${activityLogCreationFilter} ORDER BY tl.created_at DESC LIMIT 1)::UUID, t.project_id) IS TRUE THEN 1 END) AS doing,
|
||||
COUNT(CASE WHEN ${completedBetweenFilter} IS TRUE THEN 1 END) AS done
|
||||
FROM tasks t
|
||||
JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE ta.team_member_id = $1 ${archivedClause};
|
||||
`;
|
||||
|
||||
const res = await db.query(q, [teamMemberId]);
|
||||
const [d] = res.rows;
|
||||
|
||||
const total = int(d.todo) + int(d.doing) + int(d.done);
|
||||
|
||||
const chart = [
|
||||
this.createChartObject("Todo", TASK_STATUS_TODO_COLOR, d.todo),
|
||||
this.createChartObject("Doing", TASK_STATUS_DOING_COLOR, d.doing),
|
||||
this.createChartObject("Done", TASK_STATUS_DONE_COLOR, d.done),
|
||||
];
|
||||
|
||||
const data = [
|
||||
{ label: "Todo", color: TASK_STATUS_TODO_COLOR, count: d.todo },
|
||||
{ label: "Doing", color: TASK_STATUS_DOING_COLOR, count: d.doing },
|
||||
{ label: "Done", color: TASK_STATUS_DONE_COLOR, count: d.done },
|
||||
];
|
||||
|
||||
return { chart, total, data };
|
||||
}
|
||||
|
||||
protected static async getProjectsByStatus(teamId: string | null, archivedClause = ""): Promise<any> {
|
||||
const q = `WITH ProjectCounts AS (
|
||||
SELECT
|
||||
COUNT(*) AS all_projects,
|
||||
SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'Cancelled') THEN 1 ELSE 0 END) AS cancelled,
|
||||
SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'Blocked') THEN 1 ELSE 0 END) AS blocked,
|
||||
SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'On Hold') THEN 1 ELSE 0 END) AS on_hold,
|
||||
SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'Proposed') THEN 1 ELSE 0 END) AS proposed,
|
||||
SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'In Planning') THEN 1 ELSE 0 END) AS in_planning,
|
||||
SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'In Progress') THEN 1 ELSE 0 END) AS in_progress,
|
||||
SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'Completed') THEN 1 ELSE 0 END) AS completed
|
||||
FROM projects
|
||||
WHERE team_id = $1 ${archivedClause})
|
||||
|
||||
SELECT JSON_BUILD_OBJECT(
|
||||
'all_projects', all_projects,
|
||||
'cancelled', cancelled,
|
||||
'blocked', blocked,
|
||||
'on_hold', on_hold,
|
||||
'proposed', proposed,
|
||||
'in_planning', in_planning,
|
||||
'in_progress', in_progress,
|
||||
'completed', completed
|
||||
) AS counts
|
||||
FROM ProjectCounts;`;
|
||||
const res = await db.query(q, [teamId]);
|
||||
const [data] = res.rows;
|
||||
|
||||
const all = int(data?.counts.all_projects);
|
||||
const cancelled = int(data?.counts.cancelled);
|
||||
const blocked = int(data?.counts.blocked);
|
||||
const on_hold = int(data?.counts.on_hold);
|
||||
const proposed = int(data?.counts.proposed);
|
||||
const in_planning = int(data?.counts.in_planning);
|
||||
const in_progress = int(data?.counts.in_progress);
|
||||
const completed = int(data?.counts.completed);
|
||||
|
||||
const chart : IChartObject[] = [];
|
||||
|
||||
return {
|
||||
all,
|
||||
cancelled,
|
||||
blocked,
|
||||
on_hold,
|
||||
proposed,
|
||||
in_planning,
|
||||
in_progress,
|
||||
completed,
|
||||
chart
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
protected static async getProjectsByCategory(teamId: string | null, archivedClause = ""): Promise<any> {
|
||||
const q = `
|
||||
SELECT
|
||||
pc.id,
|
||||
pc.color_code AS color,
|
||||
pc.name AS label,
|
||||
COUNT(pc.id) AS count
|
||||
FROM project_categories pc
|
||||
JOIN projects ON pc.id = projects.category_id
|
||||
WHERE projects.team_id = $1 ${archivedClause}
|
||||
GROUP BY pc.id, pc.name;
|
||||
`;
|
||||
const result = await db.query(q, [teamId]);
|
||||
|
||||
const chart: IChartObject[] = [];
|
||||
|
||||
const total = result.rows.reduce((accumulator: number, current: {
|
||||
count: number
|
||||
}) => accumulator + int(current.count), 0);
|
||||
|
||||
for (const category of result.rows) {
|
||||
category.count = int(category.count);
|
||||
chart.push({
|
||||
name: category.label,
|
||||
color: category.color,
|
||||
y: category.count
|
||||
});
|
||||
}
|
||||
|
||||
return { chart, total, data: result.rows };
|
||||
|
||||
}
|
||||
|
||||
protected static async getProjectsByHealth(teamId: string | null, archivedClause = ""): Promise<any> {
|
||||
const q = `
|
||||
SELECT JSON_BUILD_OBJECT(
|
||||
'needs_attention', (SELECT COUNT(*)
|
||||
FROM projects
|
||||
WHERE team_id = $1 ${archivedClause}
|
||||
AND health_id = (SELECT id FROM sys_project_healths WHERE name = 'Needs Attention')),
|
||||
'at_risk', (SELECT COUNT(*)
|
||||
FROM projects
|
||||
WHERE team_id = $1 ${archivedClause}
|
||||
AND health_id = (SELECT id FROM sys_project_healths WHERE name = 'At Risk')),
|
||||
'good', (SELECT COUNT(*)
|
||||
FROM projects
|
||||
WHERE team_id = $1 ${archivedClause}
|
||||
AND health_id = (SELECT id FROM sys_project_healths WHERE name = 'Good')),
|
||||
'not_set', (SELECT COUNT(*)
|
||||
FROM projects
|
||||
WHERE team_id = $1 ${archivedClause}
|
||||
AND health_id = (SELECT id FROM sys_project_healths WHERE name = 'Not Set'))
|
||||
) AS counts;
|
||||
`;
|
||||
const res = await db.query(q, [teamId]);
|
||||
const [data] = res.rows;
|
||||
|
||||
const not_set = int(data?.counts.not_set);
|
||||
const needs_attention = int(data?.counts.needs_attention);
|
||||
const at_risk = int(data?.counts.at_risk);
|
||||
const good = int(data?.counts.good);
|
||||
|
||||
const chart: IChartObject[] = [];
|
||||
|
||||
return {
|
||||
not_set,
|
||||
needs_attention,
|
||||
at_risk,
|
||||
good,
|
||||
chart
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// Team Overview
|
||||
protected static createByProjectStatusChartData(body: any) {
|
||||
body.chart = [
|
||||
this.createChartObject("Cancelled", "#f37070", body.cancelled),
|
||||
this.createChartObject("Blocked", "#cbc8a1", body.blocked),
|
||||
this.createChartObject("On Hold", "#cbc8a1", body.on_hold),
|
||||
this.createChartObject("Proposed", "#cbc8a1", body.proposed),
|
||||
this.createChartObject("In Planning", "#cbc8a1", body.in_planning),
|
||||
this.createChartObject("In Progress", "#80ca79", body.in_progress),
|
||||
this.createChartObject("Completed", "#80ca79", body.completed)
|
||||
];
|
||||
}
|
||||
|
||||
protected static createByProjectHealthChartData(body: any) {
|
||||
body.chart = [
|
||||
this.createChartObject("Not Set", "#a9a9a9", body.not_set),
|
||||
this.createChartObject("Needs Attention", "#f37070", body.needs_attention),
|
||||
this.createChartObject("At Risk", "#fbc84c", body.at_risk),
|
||||
this.createChartObject("Good", "#75c997", body.good)
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
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 db from "../../../config/db";
|
||||
import { formatDuration, formatLogText, getColor, int } from "../../../shared/utils";
|
||||
import ReportingOverviewBase from "./reporting-overview-base";
|
||||
import { GroupBy, ITaskGroup } from "../../tasks-controller-base";
|
||||
import TasksControllerV2, { TaskListGroup } from "../../tasks-controller-v2";
|
||||
import { TASK_PRIORITY_COLOR_ALPHA } from "../../../shared/constants";
|
||||
import { ReportingExportModel } from "../../../models/reporting-export";
|
||||
import moment from "moment";
|
||||
import ReportingControllerBase from "../reporting-controller-base";
|
||||
|
||||
export default class ReportingOverviewController extends ReportingOverviewBase {
|
||||
@HandleExceptions()
|
||||
public static async getStatistics(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const teamId = this.getCurrentTeamId(req);
|
||||
const includeArchived = req.query.archived === "true";
|
||||
|
||||
const archivedClause = includeArchived
|
||||
? ""
|
||||
: `AND projects.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = projects.id AND user_id = '${req.user?.id}') `;
|
||||
|
||||
const teams = await this.getTeamsCounts(teamId, archivedClause);
|
||||
const projects = await this.getProjectsCounts(teamId, archivedClause);
|
||||
const members = await this.getMemberCounts(teamId);
|
||||
|
||||
projects.count = teams.projects;
|
||||
members.count = teams.members;
|
||||
|
||||
const body = {
|
||||
teams,
|
||||
projects,
|
||||
members
|
||||
};
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, body));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTeams(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const teamId = this.getCurrentTeamId(req);
|
||||
const includeArchived = req.query.archived === "true";
|
||||
|
||||
const archivedClause = includeArchived
|
||||
? ""
|
||||
: `AND id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = projects.id AND archived_projects.user_id = '${req.user?.id}')`;
|
||||
|
||||
const q = `
|
||||
SELECT id,
|
||||
name,
|
||||
COALESCE((SELECT COUNT(*) FROM projects WHERE team_id = teams.id ${archivedClause}), 0) AS projects_count,
|
||||
(SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
|
||||
FROM (
|
||||
--
|
||||
SELECT (SELECT name
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = tm.id),
|
||||
u.avatar_url
|
||||
FROM team_members tm
|
||||
LEFT JOIN users u ON tm.user_id = u.id
|
||||
WHERE team_id = teams.id
|
||||
--
|
||||
) rec) AS members
|
||||
FROM teams
|
||||
WHERE in_organization(id, $1)
|
||||
ORDER BY name;
|
||||
`;
|
||||
const result = await db.query(q, [teamId]);
|
||||
|
||||
for (const team of result.rows) {
|
||||
team.members = this.createTagList(team?.members);
|
||||
team.members.map((a: any) => a.color_code = getColor(a.name));
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjects(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 = req.query.team as string;
|
||||
|
||||
const archivedClause = archived
|
||||
? ""
|
||||
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
|
||||
|
||||
const teamFilterClause = `p.team_id = $1`;
|
||||
|
||||
const result = await ReportingControllerBase.getProjectsByTeam(teamId, size, offset, searchQuery, sortField, sortOrder, "", "", "", archivedClause, teamFilterClause, "");
|
||||
|
||||
|
||||
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[0];
|
||||
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));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjectsByTeamOrMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const teamId = req.params.team_id?.trim() || null;
|
||||
const teamMemberId = (req.query.member as string)?.trim() || null;
|
||||
const teamMemberFilter = teamId === "undefined" ? `AND pm.team_member_id = $1` : teamMemberId ? `AND pm.team_member_id = $2` : "";
|
||||
const teamIdFilter = teamId === "undefined" ? "p.team_id IS NOT NULL" : `p.team_id = $1`;
|
||||
|
||||
const q = `
|
||||
SELECT p.id,
|
||||
p.name,
|
||||
p.color_code,
|
||||
p.team_id,
|
||||
p.status_id
|
||||
FROM projects p
|
||||
LEFT JOIN project_members pm ON pm.project_id = p.id
|
||||
WHERE ${teamIdFilter} ${teamMemberFilter}
|
||||
GROUP BY p.id, p.name;`;
|
||||
|
||||
const params = teamId === "undefined" ? [teamMemberId] : teamMemberId ? [teamId, teamMemberId] : [teamId];
|
||||
|
||||
const result = await db.query(q, params);
|
||||
|
||||
const data = result.rows;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getMembersByTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const teamId = req.params.team_id?.trim() || null;
|
||||
const archived = req.query.archived === "true";
|
||||
|
||||
const pmArchivedClause = archived ? `` : `AND project_members.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = project_members.project_id AND user_id = '${req.user?.id}')`;
|
||||
const taArchivedClause = archived ? `` : `AND (SELECT tasks.project_id FROM tasks WHERE tasks.id = tasks_assignees.task_id) NOT IN (SELECT project_id FROM archived_projects WHERE project_id = (SELECT tasks.project_id FROM tasks WHERE tasks.id = tasks_assignees.task_id) AND user_id = '${req.user?.id}')`;
|
||||
|
||||
const q = `
|
||||
SELECT team_member_id AS id,
|
||||
name,
|
||||
email,
|
||||
|
||||
(SELECT COUNT(*)
|
||||
FROM project_members
|
||||
WHERE project_members.team_member_id = team_member_info_view.team_member_id ${pmArchivedClause}) AS projects,
|
||||
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
WHERE tasks_assignees.team_member_id = team_member_info_view.team_member_id ${taArchivedClause}) AS tasks,
|
||||
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
WHERE tasks_assignees.team_member_id = team_member_info_view.team_member_id
|
||||
AND is_overdue(task_id) IS TRUE ${taArchivedClause}) AS overdue,
|
||||
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
WHERE tasks_assignees.team_member_id = team_member_info_view.team_member_id
|
||||
AND task_id IN (SELECT id
|
||||
FROM tasks
|
||||
WHERE is_completed(tasks.status_id, tasks.project_id)) ${taArchivedClause}) AS completed,
|
||||
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
WHERE tasks_assignees.team_member_id = team_member_info_view.team_member_id
|
||||
AND task_id IN (SELECT id
|
||||
FROM tasks
|
||||
WHERE is_doing(tasks.status_id, tasks.project_id)) ${taArchivedClause}) AS ongoing
|
||||
|
||||
FROM team_member_info_view
|
||||
WHERE team_id = $1
|
||||
ORDER BY name;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [teamId]);
|
||||
|
||||
for (const member of result.rows) {
|
||||
member.projects = int(member.projects);
|
||||
member.tasks = int(member.tasks);
|
||||
member.overdue = int(member.overdue);
|
||||
member.completed = int(member.completed);
|
||||
member.ongoing = int(member.ongoing);
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjectOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const projectId = req.params.project_id || null;
|
||||
|
||||
const stats = await this.getProjectStats(projectId);
|
||||
const byStatus = await this.getTasksByStatus(projectId);
|
||||
const byPriority = await this.getTasksByPriority(projectId);
|
||||
const byDue = await this.getTaskCountsByDue(projectId);
|
||||
|
||||
byPriority.all = byStatus.all;
|
||||
|
||||
byDue.all = byStatus.all;
|
||||
byDue.completed = stats.completed;
|
||||
byDue.overdue = stats.overdue;
|
||||
|
||||
const body = {
|
||||
stats,
|
||||
by_status: byStatus,
|
||||
by_priority: byPriority,
|
||||
by_due: byDue
|
||||
};
|
||||
|
||||
this.createByStatusChartData(body.by_status);
|
||||
this.createByPriorityChartData(body.by_priority);
|
||||
this.createByDueDateChartData(body.by_due);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, body));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjectMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const projectId = req.params.project_id?.trim() || null;
|
||||
|
||||
const members = await ReportingExportModel.getProjectMembers(projectId as string);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, members));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjectTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const groupBy = (req.query.group || GroupBy.STATUS) as string;
|
||||
const projectId = req.params.project_id?.trim() || null;
|
||||
|
||||
const groups = await TasksControllerV2.getGroups(groupBy, projectId as string);
|
||||
const tasks = await this.getAllTasks(projectId);
|
||||
|
||||
const map = groups.reduce((g: { [x: string]: ITaskGroup }, group) => {
|
||||
if (group.id)
|
||||
g[group.id] = new TaskListGroup(group);
|
||||
return g;
|
||||
}, {});
|
||||
|
||||
TasksControllerV2.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));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTeamMemberOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const teamMemberId = req.query.teamMemberId as string;
|
||||
const archived = req.query.archived === "true";
|
||||
|
||||
const stats = await this.getTeamMemberStats(teamMemberId, archived, req.user?.id as string);
|
||||
const byStatus = await this.getTasksByStatusOfTeamMemberOverview(teamMemberId, archived, req.user?.id as string);
|
||||
const byProject = await this.getTasksByProjectOfTeamMemberOverview(teamMemberId, archived, req.user?.id as string);
|
||||
const byPriority = await this.getTasksByPriorityOfTeamMemberOverview(teamMemberId, archived, req.user?.id as string);
|
||||
|
||||
stats.projects = await this.getProjectCountOfTeamMember(teamMemberId, archived, req.user?.id as string);
|
||||
|
||||
const body = {
|
||||
stats,
|
||||
by_status: byStatus,
|
||||
by_project: byProject,
|
||||
by_priority: byPriority
|
||||
};
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, body));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getMemberOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const teamMemberId = req.query.teamMemberId as string;
|
||||
const { duration, date_range } = req.query;
|
||||
const archived = req.query.archived === "true";
|
||||
|
||||
let dateRange: string[] = [];
|
||||
if (typeof date_range === "string") {
|
||||
dateRange = date_range.split(",");
|
||||
}
|
||||
|
||||
const stats = await this.getMemberStats(teamMemberId, duration as string, dateRange, archived, req.user?.id as string);
|
||||
const byStatus = await this.getTasksByStatusOfTeamMember(teamMemberId, duration as string, dateRange, archived, req.user?.id as string);
|
||||
const byProject = await this.getTasksByProjectOfTeamMember(teamMemberId, duration as string, dateRange, archived, req.user?.id as string);
|
||||
const byPriority = await this.getTasksByPriorityOfTeamMember(teamMemberId, duration as string, dateRange, archived, req.user?.id as string);
|
||||
|
||||
stats.teams = await this.getTeamCountOfTeamMember(teamMemberId);
|
||||
stats.projects = await this.getProjectCountOfTeamMember(teamMemberId, archived, req.user?.id as string);
|
||||
|
||||
const body = {
|
||||
stats,
|
||||
by_status: byStatus,
|
||||
by_project: byProject,
|
||||
by_priority: byPriority
|
||||
};
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, body));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getMemberTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const teamMemberId = req.params.team_member_id?.trim() || null;
|
||||
const projectId = (req.query.project as string)?.trim() || null;
|
||||
const onlySingleMember = req.query.only_single_member as string;
|
||||
const { duration, date_range } = req.query;
|
||||
const includeArchived = req.query.archived === "true";
|
||||
|
||||
let dateRange: string[] = [];
|
||||
if (typeof date_range === "string") {
|
||||
dateRange = date_range.split(",");
|
||||
}
|
||||
|
||||
const results = await ReportingExportModel.getMemberTasks(teamMemberId as string, projectId, onlySingleMember, duration as string, dateRange, includeArchived, req.user?.id as string);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, results));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTeamOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const teamId = req.params.team_id || null;
|
||||
const archived = req.query.archived === "true";
|
||||
|
||||
const archivedClause = await this.getArchivedProjectsClause(archived, req.user?.id as string, "projects.id");
|
||||
|
||||
const byStatus = await this.getProjectsByStatus(teamId, archivedClause);
|
||||
const byCategory = await this.getProjectsByCategory(teamId, archivedClause);
|
||||
const byHealth = await this.getProjectsByHealth(teamId, archivedClause);
|
||||
|
||||
byCategory.all = byStatus.all;
|
||||
byHealth.all = byStatus.all;
|
||||
|
||||
const body = {
|
||||
by_status: byStatus,
|
||||
by_category: byCategory,
|
||||
by_health: byHealth
|
||||
};
|
||||
|
||||
this.createByProjectStatusChartData(body.by_status);
|
||||
this.createByProjectHealthChartData(body.by_health);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, body));
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,581 @@
|
||||
import HandleExceptions from "../../../decorators/handle-exceptions";
|
||||
import { IWorkLenzRequest } from "../../../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../../../interfaces/worklenz-response";
|
||||
import ReportingOverviewBase from "./reporting-overview-base";
|
||||
import { ReportingExportModel } from "../../../models/reporting-export";
|
||||
import { formatDuration, formatLogText, getColor, int } from "../../../shared/utils";
|
||||
import moment from "moment";
|
||||
import Excel from "exceljs";
|
||||
import ReportingControllerBase from "../reporting-controller-base";
|
||||
import { TASK_PRIORITY_COLOR_ALPHA } from "../../../shared/constants";
|
||||
|
||||
export default class ReportingOverviewExportController extends ReportingOverviewBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjects(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 = req.query.team as string;
|
||||
|
||||
const archivedClause = archived
|
||||
? ""
|
||||
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
|
||||
|
||||
const teamFilterClause = `p.team_id = $1`;
|
||||
|
||||
const result = await ReportingControllerBase.getProjectsByTeam(teamId, size, offset, searchQuery, sortField, sortOrder, "", "", "", archivedClause, teamFilterClause, "");
|
||||
|
||||
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[0];
|
||||
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 result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjectsByTeamOrMember(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
const teamId = (req.query.team_id as string)?.trim() || null;
|
||||
const teamName = (req.query.team_name as string)?.trim() || null;
|
||||
|
||||
const result = await ReportingControllerBase.exportProjects(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 result.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 getMembersByTeam(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
const teamId = (req.query.team_id as string)?.trim() || null;
|
||||
const teamName = (req.query.team_name as string)?.trim() || null;
|
||||
|
||||
const result = await ReportingExportModel.getMembersByTeam(teamId);
|
||||
|
||||
for (const member of result) {
|
||||
member.projects = int(member.projects);
|
||||
member.tasks = int(member.tasks);
|
||||
member.overdue = int(member.overdue);
|
||||
member.completed = int(member.completed);
|
||||
member.ongoing = int(member.ongoing);
|
||||
}
|
||||
|
||||
// excel file
|
||||
const exportDate = moment().format("MMM-DD-YYYY");
|
||||
const fileName = `${teamName} members - ${exportDate}`;
|
||||
const workbook = new Excel.Workbook();
|
||||
|
||||
const sheet = workbook.addWorksheet("Members");
|
||||
|
||||
// define columns in table
|
||||
sheet.columns = [
|
||||
{ header: "Name", key: "name", width: 30 },
|
||||
{ header: "Email", key: "email", width: 20 },
|
||||
{ header: "Projects", key: "projects", width: 20 },
|
||||
{ header: "Tasks", key: "tasks", width: 20 },
|
||||
{ header: "Overdue Tasks", key: "overdue_tasks", width: 20 },
|
||||
{ header: "Completed Tasks", key: "completed_tasks", width: 20 },
|
||||
{ header: "Ongoing Tasks", key: "ongoing_tasks", width: 20 },
|
||||
];
|
||||
|
||||
// set title
|
||||
sheet.getCell("A1").value = `Members from ${teamName}`;
|
||||
sheet.mergeCells("A1:G1");
|
||||
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:G2");
|
||||
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 = ["Name", "Email", "Projects", "Tasks", "Overdue Tasks", "Completed Tasks", "Ongoing Tasks"];
|
||||
sheet.getRow(4).font = { bold: true };
|
||||
|
||||
// set table data
|
||||
for (const item of result) {
|
||||
sheet.addRow({
|
||||
name: item.name,
|
||||
email: item.email ? item.email : "-",
|
||||
projects: item.projects ? item.projects.toString() : "-",
|
||||
tasks: item.tasks ? item.tasks.toString() : "-",
|
||||
overdue_tasks: item.overdue ? item.overdue.toString() : "-",
|
||||
completed_tasks: item.completed ? item.completed.toString() : "-",
|
||||
ongoing_tasks: item.ongoing ? item.ongoing.toString() : "-",
|
||||
});
|
||||
}
|
||||
|
||||
// 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 exportProjectMembers(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
const projectId = (req.query.project_id as string)?.trim() || null;
|
||||
const projectName = (req.query.project_name as string)?.trim() || null;
|
||||
const teamName = (req.query.team_name as string)?.trim() || "";
|
||||
|
||||
const results = await ReportingExportModel.getProjectMembers(projectId as string);
|
||||
|
||||
// excel file
|
||||
const exportDate = moment().format("MMM-DD-YYYY");
|
||||
const fileName = `${teamName} ${projectName} members - ${exportDate}`;
|
||||
const workbook = new Excel.Workbook();
|
||||
|
||||
const sheet = workbook.addWorksheet("Members");
|
||||
|
||||
// define columns in table
|
||||
sheet.columns = [
|
||||
{ header: "Name", key: "name", width: 30 },
|
||||
{ header: "Tasks Count", key: "tasks_count", width: 20 },
|
||||
{ header: "Completed Tasks", key: "completed_tasks", width: 20 },
|
||||
{ header: "Incomplete Tasks", key: "incomplete_tasks", width: 20 },
|
||||
{ header: "Overdue Tasks", key: "overdue_tasks", width: 20 },
|
||||
{ header: "Contribution(%)", key: "contribution", width: 20 },
|
||||
{ header: "Progress(%)", key: "progress", width: 20 },
|
||||
{ header: "Logged Time", key: "logged_time", width: 20 },
|
||||
];
|
||||
|
||||
// set title
|
||||
sheet.getCell("A1").value = `Members from ${projectName} - ${teamName}`;
|
||||
sheet.mergeCells("A1:H1");
|
||||
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:H2");
|
||||
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 = ["Name", "Tasks Count", "Completed Tasks", "Incomplete Tasks", "Overdue Tasks", "Contribution(%)", "Progress(%)", "Logged Time"];
|
||||
sheet.getRow(4).font = { bold: true };
|
||||
|
||||
// set table data
|
||||
for (const item of results) {
|
||||
sheet.addRow({
|
||||
name: item.name,
|
||||
tasks_count: item.tasks_count ? item.tasks_count : "-",
|
||||
completed_tasks: item.completed ? item.completed : "-",
|
||||
incomplete_tasks: item.incompleted ? item.incompleted : "-",
|
||||
overdue_tasks: item.overdue ? item.overdue : "-",
|
||||
contribution: item.contribution ? item.contribution : "-",
|
||||
progress: item.progress ? item.progress : "-",
|
||||
logged_time: item.time_logged ? item.time_logged : "-",
|
||||
});
|
||||
}
|
||||
|
||||
// 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 exportProjectTasks(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
const projectId = (req.query.project_id as string)?.trim() || null;
|
||||
const projectName = (req.query.project_name as string)?.trim() || null;
|
||||
const teamName = (req.query.team_name as string)?.trim() || "";
|
||||
|
||||
const results = await this.getAllTasks(projectId);
|
||||
|
||||
// excel file
|
||||
const exportDate = moment().format("MMM-DD-YYYY");
|
||||
const fileName = `${teamName} ${projectName} tasks - ${exportDate}`;
|
||||
const workbook = new Excel.Workbook();
|
||||
|
||||
const sheet = workbook.addWorksheet("Tasks");
|
||||
|
||||
// define columns in table
|
||||
sheet.columns = [
|
||||
{ header: "Task", key: "task", width: 30 },
|
||||
{ header: "Status", key: "status", width: 20 },
|
||||
{ header: "Priority", key: "priority", width: 20 },
|
||||
{ header: "Phase", key: "phase", width: 20 },
|
||||
{ header: "Due Date", key: "due_date", width: 20 },
|
||||
{ header: "Completed On", key: "completed_on", width: 20 },
|
||||
{ header: "Days Overdue", key: "days_overdue", width: 20 },
|
||||
{ header: "Estimated Time", key: "estimated_time", width: 20 },
|
||||
{ header: "Logged Time", key: "logged_time", width: 20 },
|
||||
{ header: "Overlogged Time", key: "overlogged_time", width: 20 },
|
||||
];
|
||||
|
||||
// set title
|
||||
sheet.getCell("A1").value = `Tasks from ${projectName} - ${teamName}`;
|
||||
sheet.mergeCells("A1:J1");
|
||||
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:J2");
|
||||
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 = ["Task", "Status", "Priority", "Phase", "Due Date", "Completed On", "Days Overdue", "Estimated Time", "Logged Time", "Overlogged Time"];
|
||||
sheet.getRow(4).font = { bold: true };
|
||||
|
||||
// set table data
|
||||
for (const item of results) {
|
||||
const time_spent = { hours: ~~(item.total_minutes_spent / 60), minutes: item.total_minutes_spent % 60 };
|
||||
item.total_minutes_spent = Math.ceil(item.total_seconds_spent / 60);
|
||||
|
||||
sheet.addRow({
|
||||
task: item.name,
|
||||
status: item.status_name ? item.status_name : "-",
|
||||
priority: item.priority_name ? item.priority_name : "-",
|
||||
phase: item.phase_name ? item.phase_name : "-",
|
||||
due_date: item.end_date ? moment(item.end_date).format("YYYY-MM-DD") : "-",
|
||||
completed_on: item.completed_at ? moment(item.completed_at).format("YYYY-MM-DD") : "-",
|
||||
days_overdue: item.overdue_days ? item.overdue_days : "-",
|
||||
estimated_time: item.total_minutes !== "0" ? `${~~(item.total_minutes / 60)}h ${(item.total_minutes % 60)}m` : "-",
|
||||
logged_time: item.total_minutes_spent ? `${time_spent.hours}h ${(time_spent.minutes)}m` : "-",
|
||||
overlogged_time: item.overlogged_time_string !== "0h 0m" ? item.overlogged_time_string : "-",
|
||||
});
|
||||
}
|
||||
|
||||
// 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 exportMemberTasks(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
const teamMemberId = (req.query.team_member_id as string)?.trim() || null;
|
||||
const teamMemberName = (req.query.team_member_name as string)?.trim() || null;
|
||||
const teamName = (req.query.team_name as string)?.trim() || "";
|
||||
|
||||
const { duration, date_range, only_single_member, archived} = req.query;
|
||||
|
||||
const includeArchived = req.query.archived === "true";
|
||||
|
||||
let dateRange: string[] = [];
|
||||
if (typeof date_range === "string") {
|
||||
dateRange = date_range.split(",");
|
||||
}
|
||||
|
||||
const results = await ReportingExportModel.getMemberTasks(teamMemberId as string, null, only_single_member as string, duration as string, dateRange, includeArchived, req.user?.id as string);
|
||||
|
||||
// excel file
|
||||
const exportDate = moment().format("MMM-DD-YYYY");
|
||||
const fileName = `${teamMemberName} tasks - ${exportDate}`;
|
||||
const workbook = new Excel.Workbook();
|
||||
|
||||
const sheet = workbook.addWorksheet("Tasks");
|
||||
|
||||
// define columns in table
|
||||
sheet.columns = [
|
||||
{ header: "Task", key: "task", width: 30 },
|
||||
{ header: "Project", key: "project", width: 20 },
|
||||
{ header: "Status", key: "status", width: 20 },
|
||||
{ header: "Priority", key: "priority", width: 20 },
|
||||
{ header: "Due Date", key: "due_date", width: 20 },
|
||||
{ header: "Completed Date", key: "completed_on", width: 20 },
|
||||
{ header: "Estimated Time", key: "estimated_time", width: 20 },
|
||||
{ header: "Logged Time", key: "logged_time", width: 20 },
|
||||
{ header: "Overlogged Time", key: "overlogged_time", width: 20 },
|
||||
];
|
||||
|
||||
// set title
|
||||
sheet.getCell("A1").value = `Tasks of ${teamMemberName} - ${teamName}`;
|
||||
sheet.mergeCells("A1:I1");
|
||||
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:I2");
|
||||
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 = ["Task", "Project", "Status", "Priority", "Due Date", "Completed Date", "Estimated Time", "Logged Time", "Overlogged Time"];
|
||||
sheet.getRow(4).font = { bold: true };
|
||||
|
||||
// set table data
|
||||
for (const item of results) {
|
||||
sheet.addRow({
|
||||
task: item.name,
|
||||
project: item.project_name ? item.project_name : "-",
|
||||
status: item.status_name ? item.status_name : "-",
|
||||
priority: item.priority_name ? item.priority_name : "-",
|
||||
due_date: item.end_date ? moment(item.end_date).format("YYYY-MM-DD") : "-",
|
||||
completed_on: item.completed_date ? moment(item.completed_date).format("YYYY-MM-DD") : "-",
|
||||
estimated_time: item.estimated_string ? item.estimated_string : "-",
|
||||
logged_time: item.time_spent_string ? item.time_spent_string : "-",
|
||||
overlogged_time: item.overlogged_time ? item.overlogged_time : "-",
|
||||
});
|
||||
}
|
||||
|
||||
// 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 exportFlatTasks(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
const teamMemberId = (req.query.team_member_id as string)?.trim() || null;
|
||||
const teamMemberName = (req.query.team_member_name as string)?.trim() || null;
|
||||
const projectId = (req.query.project_id as string)?.trim() || null;
|
||||
const projectName = (req.query.project_name as string)?.trim() || null;
|
||||
|
||||
const includeArchived = req.query.archived === "true";
|
||||
|
||||
const results = await ReportingExportModel.getMemberTasks(teamMemberId as string, projectId, "false", "", [], includeArchived, req.user?.id as string);
|
||||
|
||||
// excel file
|
||||
const exportDate = moment().format("MMM-DD-YYYY");
|
||||
const fileName = `${teamMemberName}'s tasks in ${projectName} - ${exportDate}`;
|
||||
const workbook = new Excel.Workbook();
|
||||
|
||||
const sheet = workbook.addWorksheet("Tasks");
|
||||
|
||||
// define columns in table
|
||||
sheet.columns = [
|
||||
{ header: "Task", key: "task", width: 30 },
|
||||
{ header: "Project", key: "project", width: 20 },
|
||||
{ header: "Status", key: "status", width: 20 },
|
||||
{ header: "Priority", key: "priority", width: 20 },
|
||||
{ header: "Due Date", key: "due_date", width: 20 },
|
||||
{ header: "Completed Date", key: "completed_on", width: 20 },
|
||||
{ header: "Estimated Time", key: "estimated_time", width: 20 },
|
||||
{ header: "Logged Time", key: "logged_time", width: 20 },
|
||||
{ header: "Overlogged Time", key: "overlogged_time", width: 20 },
|
||||
];
|
||||
|
||||
// set title
|
||||
sheet.getCell("A1").value = `Tasks of ${teamMemberName} in ${projectName}`;
|
||||
sheet.mergeCells("A1:I1");
|
||||
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:I2");
|
||||
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 = ["Task", "Project", "Status", "Priority", "Due Date", "Completed Date", "Estimated Time", "Logged Time", "Overlogged Time"];
|
||||
sheet.getRow(4).font = { bold: true };
|
||||
|
||||
// set table data
|
||||
for (const item of results) {
|
||||
sheet.addRow({
|
||||
task: item.name,
|
||||
project: item.project_name ? item.project_name : "-",
|
||||
status: item.status_name ? item.status_name : "-",
|
||||
priority: item.priority_name ? item.priority_name : "-",
|
||||
due_date: item.end_date ? moment(item.end_date).format("YYYY-MM-DD") : "-",
|
||||
completed_on: item.completed_date ? moment(item.completed_date).format("YYYY-MM-DD") : "-",
|
||||
estimated_time: item.estimated_string ? item.estimated_string : "-",
|
||||
logged_time: item.time_spent_string ? item.time_spent_string : "-",
|
||||
overlogged_time: item.overlogged_time ? item.overlogged_time : "-",
|
||||
});
|
||||
}
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import ReportingControllerBase from "../reporting-controller-base";
|
||||
|
||||
export default class ReportingProjectsBase extends ReportingControllerBase {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
import moment from "moment";
|
||||
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 { getColor, int, log_error } from "../../shared/utils";
|
||||
import ReportingControllerBase from "./reporting-controller-base";
|
||||
import { DATE_RANGES } from "../../shared/constants";
|
||||
import Excel from "exceljs";
|
||||
|
||||
enum IToggleOptions {
|
||||
'WORKING_DAYS' = 'WORKING_DAYS', 'MAN_DAYS' = 'MAN_DAYS'
|
||||
}
|
||||
|
||||
export default class ReportingAllocationController extends ReportingControllerBase {
|
||||
private static async getTimeLoggedByProjects(projects: string[], users: string[], key: string, dateRange: string[], archived = false, user_id = ""): Promise<any> {
|
||||
try {
|
||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||
const userIds = users.map(u => `'${u}'`).join(",");
|
||||
|
||||
const duration = this.getDateRangeClause(key || DATE_RANGES.LAST_WEEK, dateRange);
|
||||
const archivedClause = archived
|
||||
? ""
|
||||
: `AND projects.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = projects.id AND user_id = '${user_id}') `;
|
||||
|
||||
const projectTimeLogs = await this.getTotalTimeLogsByProject(archived, duration, projectIds, userIds, archivedClause);
|
||||
const userTimeLogs = await this.getTotalTimeLogsByUser(archived, duration, projectIds, userIds);
|
||||
|
||||
const format = (seconds: number) => {
|
||||
if (seconds === 0) return "-";
|
||||
const duration = moment.duration(seconds, "seconds");
|
||||
const formattedDuration = `${~~(duration.asHours())}h ${duration.minutes()}m ${duration.seconds()}s`;
|
||||
return formattedDuration;
|
||||
};
|
||||
|
||||
let totalProjectsTime = 0;
|
||||
let totalUsersTime = 0;
|
||||
|
||||
for (const project of projectTimeLogs) {
|
||||
if (project.all_tasks_count > 0) {
|
||||
project.progress = Math.round((project.completed_tasks_count / project.all_tasks_count) * 100);
|
||||
} else {
|
||||
project.progress = 0;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for (const log of project.time_logs) {
|
||||
total += log.time_logged;
|
||||
log.time_logged = format(log.time_logged);
|
||||
}
|
||||
project.totalProjectsTime = totalProjectsTime + total;
|
||||
project.total = format(total);
|
||||
}
|
||||
|
||||
for (const log of userTimeLogs) {
|
||||
log.totalUsersTime = totalUsersTime + parseInt(log.time_logged)
|
||||
log.time_logged = format(parseInt(log.time_logged));
|
||||
}
|
||||
|
||||
return { projectTimeLogs, userTimeLogs };
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private static async getTotalTimeLogsByProject(archived: boolean, duration: string, projectIds: string, userIds: string, archivedClause = "") {
|
||||
try {
|
||||
const q = `SELECT projects.name,
|
||||
projects.color_code,
|
||||
sps.name AS status_name,
|
||||
sps.color_code AS status_color_code,
|
||||
sps.icon AS status_icon,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
|
||||
AND project_id = projects.id) AS all_tasks_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
|
||||
AND project_id = projects.id
|
||||
AND status_id IN (SELECT id
|
||||
FROM task_statuses
|
||||
WHERE project_id = projects.id
|
||||
AND category_id IN
|
||||
(SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS completed_tasks_count,
|
||||
(
|
||||
SELECT COALESCE(JSON_AGG(r), '[]'::JSON)
|
||||
FROM (
|
||||
SELECT name,
|
||||
(SELECT COALESCE(SUM(time_spent), 0)
|
||||
FROM task_work_log
|
||||
LEFT JOIN tasks t ON task_work_log.task_id = t.id
|
||||
WHERE user_id = users.id
|
||||
AND CASE WHEN ($1 IS TRUE) THEN t.project_id IS NOT NULL ELSE t.archived = FALSE END
|
||||
AND t.project_id = projects.id
|
||||
${duration}) AS time_logged
|
||||
FROM users
|
||||
WHERE id IN (${userIds})
|
||||
ORDER BY name
|
||||
) r
|
||||
) AS time_logs
|
||||
FROM projects
|
||||
LEFT JOIN sys_project_statuses sps ON projects.status_id = sps.id
|
||||
WHERE projects.id IN (${projectIds}) ${archivedClause};`;
|
||||
|
||||
const result = await db.query(q, [archived]);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static async getTotalTimeLogsByUser(archived: boolean, duration: string, projectIds: string, userIds: string) {
|
||||
try {
|
||||
const q = `(SELECT id,
|
||||
(SELECT COALESCE(SUM(time_spent), 0)
|
||||
FROM task_work_log
|
||||
LEFT JOIN tasks t ON task_work_log.task_id = t.id
|
||||
WHERE user_id = users.id
|
||||
AND CASE WHEN ($1 IS TRUE) THEN t.project_id IS NOT NULL ELSE t.archived = FALSE END
|
||||
AND t.project_id IN (${projectIds})
|
||||
${duration}) AS time_logged
|
||||
FROM users
|
||||
WHERE id IN (${userIds})
|
||||
ORDER BY name);`;
|
||||
const result = await db.query(q, [archived]);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static async getUserIds(teamIds: any) {
|
||||
try {
|
||||
const q = `SELECT id, (SELECT name)
|
||||
FROM users
|
||||
WHERE id IN (SELECT user_id
|
||||
FROM team_members
|
||||
WHERE team_id IN (${teamIds}))
|
||||
GROUP BY id
|
||||
ORDER BY name`;
|
||||
const result = await db.query(q, []);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getAllocation(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const teams = (req.body.teams || []) as string[]; // ids
|
||||
|
||||
const teamIds = teams.map(id => `'${id}'`).join(",");
|
||||
const projectIds = (req.body.projects || []) as string[];
|
||||
|
||||
if (!teamIds || !projectIds.length)
|
||||
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
|
||||
|
||||
const users = await this.getUserIds(teamIds);
|
||||
const userIds = users.map((u: any) => u.id);
|
||||
|
||||
const { projectTimeLogs, userTimeLogs } = await this.getTimeLoggedByProjects(projectIds, userIds, req.body.duration, req.body.date_range, (req.query.archived === "true"), req.user?.id);
|
||||
|
||||
for (const [i, user] of users.entries()) {
|
||||
user.total_time = userTimeLogs[i].time_logged;
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, { users, projects: projectTimeLogs }));
|
||||
}
|
||||
|
||||
public static formatDurationDate = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
@HandleExceptions()
|
||||
public static async export(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
const teams = (req.query.teams as string)?.split(",");
|
||||
const teamIds = teams.map(t => `'${t}'`).join(",");
|
||||
|
||||
const projectIds = (req.query.projects as string)?.split(",");
|
||||
|
||||
const duration = req.query.duration;
|
||||
|
||||
const dateRange = (req.query.date_range as string)?.split(",");
|
||||
|
||||
let start = "-";
|
||||
let end = "-";
|
||||
|
||||
if (dateRange.length === 2) {
|
||||
start = dateRange[0] ? this.formatDurationDate(new Date(dateRange[0])).toString() : "-";
|
||||
end = dateRange[1] ? this.formatDurationDate(new Date(dateRange[1])).toString() : "-";
|
||||
} else {
|
||||
switch (duration) {
|
||||
case DATE_RANGES.YESTERDAY:
|
||||
start = moment().subtract(1, "day").format("YYYY-MM-DD").toString();
|
||||
break;
|
||||
case DATE_RANGES.LAST_WEEK:
|
||||
start = moment().subtract(1, "week").format("YYYY-MM-DD").toString();
|
||||
break;
|
||||
case DATE_RANGES.LAST_MONTH:
|
||||
start = moment().subtract(1, "month").format("YYYY-MM-DD").toString();
|
||||
break;
|
||||
case DATE_RANGES.LAST_QUARTER:
|
||||
start = moment().subtract(3, "months").format("YYYY-MM-DD").toString();
|
||||
break;
|
||||
}
|
||||
end = moment().format("YYYY-MM-DD").toString();
|
||||
}
|
||||
|
||||
const users = await this.getUserIds(teamIds);
|
||||
const userIds = users.map((u: any) => u.id);
|
||||
|
||||
const { projectTimeLogs, userTimeLogs } = await this.getTimeLoggedByProjects(projectIds, userIds, duration as string, dateRange, (req.query.include_archived === "true"), req.user?.id);
|
||||
|
||||
for (const [i, user] of users.entries()) {
|
||||
user.total_time = userTimeLogs[i].time_logged;
|
||||
}
|
||||
|
||||
// excel file
|
||||
const exportDate = moment().format("MMM-DD-YYYY");
|
||||
const fileName = `Reporting Time Sheet - ${exportDate}`;
|
||||
const workbook = new Excel.Workbook();
|
||||
|
||||
const sheet = workbook.addWorksheet("Reporting Time Sheet");
|
||||
|
||||
sheet.columns = [
|
||||
{ header: "Project", key: "project", width: 25 },
|
||||
{ header: "Logged Time", key: "logged_time", width: 20 },
|
||||
{ header: "Total", key: "total", width: 25 },
|
||||
];
|
||||
|
||||
sheet.getCell("A1").value = `Reporting Time Sheet`;
|
||||
sheet.mergeCells("A1:G1");
|
||||
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:G2");
|
||||
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
|
||||
sheet.getCell("A3").value = `From : ${start} To : ${end}`;
|
||||
sheet.mergeCells("A3:D3");
|
||||
|
||||
let totalProjectTime = 0;
|
||||
let totalMemberTime = 0;
|
||||
|
||||
if (projectTimeLogs.length > 0) {
|
||||
const rowTop = sheet.getRow(5);
|
||||
rowTop.getCell(1).value = "";
|
||||
|
||||
users.forEach((user: { id: string, name: string, total_time: string }, index: any) => {
|
||||
rowTop.getCell(index + 2).value = user.name;
|
||||
});
|
||||
|
||||
rowTop.getCell(users.length + 2).value = "Total";
|
||||
|
||||
rowTop.font = {
|
||||
bold: true
|
||||
};
|
||||
|
||||
for (const project of projectTimeLogs) {
|
||||
|
||||
const rowValues = [];
|
||||
rowValues[1] = project.name;
|
||||
project.time_logs.forEach((log: any, index: any) => {
|
||||
rowValues[index + 2] = log.time_logged === "0h 0m 0s" ? "-" : log.time_logged;
|
||||
});
|
||||
rowValues[project.time_logs.length + 2] = project.total;
|
||||
sheet.addRow(rowValues);
|
||||
|
||||
const { lastRow } = sheet;
|
||||
if (lastRow) {
|
||||
const totalCell = lastRow.getCell(project.time_logs.length + 2);
|
||||
totalCell.style.font = { bold: true };
|
||||
}
|
||||
totalProjectTime = totalProjectTime + project.totalProjectsTime
|
||||
}
|
||||
|
||||
const rowBottom = sheet.getRow(projectTimeLogs.length + 6);
|
||||
rowBottom.getCell(1).value = "Total";
|
||||
rowBottom.getCell(1).style.font = { bold: true };
|
||||
userTimeLogs.forEach((log: { id: string, time_logged: string, totalUsersTime: number }, index: any) => {
|
||||
totalMemberTime = totalMemberTime + log.totalUsersTime
|
||||
rowBottom.getCell(index + 2).value = log.time_logged;
|
||||
});
|
||||
rowBottom.font = {
|
||||
bold: true
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
const format = (seconds: number) => {
|
||||
if (seconds === 0) return "-";
|
||||
const duration = moment.duration(seconds, "seconds");
|
||||
const formattedDuration = `${~~(duration.asHours())}h ${duration.minutes()}m ${duration.seconds()}s`;
|
||||
return formattedDuration;
|
||||
};
|
||||
|
||||
const projectTotalTimeRow = sheet.getRow(projectTimeLogs.length + 8);
|
||||
projectTotalTimeRow.getCell(1).value = "Total logged time of Projects"
|
||||
projectTotalTimeRow.getCell(2).value = `${format(totalProjectTime)}`
|
||||
projectTotalTimeRow.getCell(1).style.font = { bold: true };
|
||||
projectTotalTimeRow.getCell(2).style.font = { bold: true };
|
||||
|
||||
const membersTotalTimeRow = sheet.getRow(projectTimeLogs.length + 9);
|
||||
membersTotalTimeRow.getCell(1).value = "Total logged time of Members"
|
||||
membersTotalTimeRow.getCell(2).value = `${format(totalMemberTime)}`
|
||||
membersTotalTimeRow.getCell(1).style.font = { bold: true };
|
||||
membersTotalTimeRow.getCell(2).style.font = { bold: true };
|
||||
|
||||
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 getProjectTimeSheets(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const archived = req.query.archived === "true";
|
||||
|
||||
const teams = (req.body.teams || []) as string[]; // ids
|
||||
const teamIds = teams.map(id => `'${id}'`).join(",");
|
||||
|
||||
const projects = (req.body.projects || []) as string[];
|
||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||
|
||||
if (!teamIds || !projectIds.length)
|
||||
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
|
||||
|
||||
const { duration, date_range } = req.body;
|
||||
|
||||
const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range);
|
||||
|
||||
const archivedClause = archived
|
||||
? ""
|
||||
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
|
||||
|
||||
const q = `
|
||||
SELECT p.id,
|
||||
p.name,
|
||||
(SELECT SUM(time_spent)) AS logged_time,
|
||||
SUM(total_minutes) AS estimated,
|
||||
color_code
|
||||
FROM projects p
|
||||
LEFT JOIN tasks t ON t.project_id = p.id
|
||||
LEFT JOIN task_work_log ON task_work_log.task_id = t.id
|
||||
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
|
||||
GROUP BY p.id, p.name
|
||||
ORDER BY logged_time DESC;`;
|
||||
const result = await db.query(q, []);
|
||||
|
||||
const data = [];
|
||||
|
||||
for (const project of result.rows) {
|
||||
project.value = project.logged_time ? parseFloat(moment.duration(project.logged_time, "seconds").asHours().toFixed(2)) : 0;
|
||||
project.estimated_value = project.estimated ? parseFloat(moment.duration(project.estimated, "minutes").asHours().toFixed(2)) : 0;
|
||||
|
||||
if (project.value > 0 ) {
|
||||
data.push(project);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getMemberTimeSheets(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const archived = req.query.archived === "true";
|
||||
|
||||
const teams = (req.body.teams || []) as string[]; // ids
|
||||
const teamIds = teams.map(id => `'${id}'`).join(",");
|
||||
|
||||
const projects = (req.body.projects || []) as string[];
|
||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||
|
||||
if (!teamIds || !projectIds.length)
|
||||
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
|
||||
|
||||
const { duration, date_range } = req.body;
|
||||
|
||||
const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range);
|
||||
const archivedClause = archived
|
||||
? ""
|
||||
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
|
||||
|
||||
const q = `
|
||||
SELECT tmiv.email, tmiv.name, SUM(time_spent) AS logged_time
|
||||
FROM team_member_info_view tmiv
|
||||
LEFT JOIN task_work_log ON task_work_log.user_id = tmiv.user_id
|
||||
LEFT JOIN tasks t ON t.id = task_work_log.task_id
|
||||
LEFT JOIN projects p ON p.id = t.project_id AND p.team_id = tmiv.team_id
|
||||
WHERE p.id IN (${projectIds})
|
||||
${durationClause} ${archivedClause}
|
||||
GROUP BY tmiv.email, tmiv.name
|
||||
ORDER BY logged_time DESC;`;
|
||||
const result = await db.query(q, []);
|
||||
|
||||
for (const member of result.rows) {
|
||||
member.value = member.logged_time ? parseFloat(moment.duration(member.logged_time, "seconds").asHours().toFixed(2)) : 0;
|
||||
member.color_code = getColor(member.name);
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
private static getEstimated(project: any, type: string) {
|
||||
|
||||
switch (type) {
|
||||
case IToggleOptions.MAN_DAYS:
|
||||
return project.estimated_man_days ?? 0;;
|
||||
|
||||
case IToggleOptions.WORKING_DAYS:
|
||||
return project.estimated_working_days ?? 0;;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getEstimatedVsActual(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const archived = req.query.archived === "true";
|
||||
|
||||
const teams = (req.body.teams || []) as string[]; // ids
|
||||
const teamIds = teams.map(id => `'${id}'`).join(",");
|
||||
|
||||
const projects = (req.body.projects || []) as string[];
|
||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||
const { type } = req.body;
|
||||
|
||||
if (!teamIds || !projectIds.length)
|
||||
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
|
||||
|
||||
const { duration, date_range } = req.body;
|
||||
|
||||
const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range);
|
||||
|
||||
const archivedClause = archived
|
||||
? ""
|
||||
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
|
||||
|
||||
const q = `
|
||||
SELECT p.id,
|
||||
p.name,
|
||||
p.end_date,
|
||||
p.hours_per_day::INT,
|
||||
p.estimated_man_days::INT,
|
||||
p.estimated_working_days::INT,
|
||||
(SELECT SUM(time_spent)) AS logged_time,
|
||||
(SELECT COALESCE(SUM(total_minutes), 0)
|
||||
FROM tasks
|
||||
WHERE project_id = p.id) AS estimated,
|
||||
color_code
|
||||
FROM projects p
|
||||
LEFT JOIN tasks t ON t.project_id = p.id
|
||||
LEFT JOIN task_work_log ON task_work_log.task_id = t.id
|
||||
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
|
||||
GROUP BY p.id, p.name
|
||||
ORDER BY logged_time DESC;`;
|
||||
const result = await db.query(q, []);
|
||||
|
||||
const data = [];
|
||||
|
||||
for (const project of result.rows) {
|
||||
const durationInHours = parseFloat(moment.duration(project.logged_time, "seconds").asHours().toFixed(2));
|
||||
const hoursPerDay = parseInt(project.hours_per_day ?? 1);
|
||||
|
||||
project.value = parseFloat((durationInHours / hoursPerDay).toFixed(2)) ?? 0;
|
||||
|
||||
project.estimated_value = this.getEstimated(project, type);
|
||||
project.estimated_man_days = project.estimated_man_days ?? 0;
|
||||
project.estimated_working_days = project.estimated_working_days ?? 0;
|
||||
project.hours_per_day = project.hours_per_day ?? 0;
|
||||
|
||||
if (project.value > 0 || project.estimated_value > 0 ) {
|
||||
data.push(project);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,604 @@
|
||||
import WorklenzControllerBase from "../worklenz-controller-base";
|
||||
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
|
||||
import db from "../../config/db";
|
||||
import moment from "moment";
|
||||
import { DATE_RANGES, TASK_PRIORITY_COLOR_ALPHA } from "../../shared/constants";
|
||||
import { formatDuration, formatLogText, getColor, int } from "../../shared/utils";
|
||||
|
||||
export default abstract class ReportingControllerBase extends WorklenzControllerBase {
|
||||
protected static getPercentage(n: number, total: number) {
|
||||
return +(n ? (n / total) * 100 : 0).toFixed();
|
||||
}
|
||||
|
||||
protected static getCurrentTeamId(req: IWorkLenzRequest): string | null {
|
||||
return req.user?.team_id ?? null;
|
||||
}
|
||||
|
||||
protected static async getTotalTasksCount(projectId: string | null) {
|
||||
const q = `
|
||||
SELECT COUNT(*) AS count
|
||||
FROM tasks
|
||||
WHERE project_id = $1;
|
||||
`;
|
||||
const result = await db.query(q, [projectId]);
|
||||
const [data] = result.rows;
|
||||
return data.count || 0;
|
||||
}
|
||||
|
||||
protected static async getArchivedProjectsClause(archived = false, user_id: string, column_name: string) {
|
||||
return archived
|
||||
? ""
|
||||
: `AND ${column_name} NOT IN (SELECT project_id FROM archived_projects WHERE project_id = ${column_name} AND user_id = '${user_id}') `;
|
||||
}
|
||||
|
||||
protected static async getAllTasks(projectId: string | null) {
|
||||
const q = `
|
||||
SELECT id,
|
||||
name,
|
||||
parent_task_id,
|
||||
parent_task_id IS NOT NULL AS is_sub_task,
|
||||
status_id AS status,
|
||||
(SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status_name,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color,
|
||||
priority_id AS priority,
|
||||
(SELECT value FROM task_priorities WHERE id = tasks.priority_id) AS priority_value,
|
||||
(SELECT name FROM task_priorities WHERE id = tasks.priority_id) AS priority_name,
|
||||
(SELECT color_code FROM task_priorities WHERE id = tasks.priority_id) AS priority_color,
|
||||
end_date,
|
||||
(SELECT phase_id FROM task_phase WHERE task_id = tasks.id) AS phase_id,
|
||||
(SELECT name
|
||||
FROM project_phases
|
||||
WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = tasks.id)) AS phase_name,
|
||||
completed_at,
|
||||
total_minutes,
|
||||
(SELECT SUM(time_spent) FROM task_work_log WHERE task_id = tasks.id) AS total_seconds_spent
|
||||
FROM tasks
|
||||
WHERE project_id = $1
|
||||
ORDER BY name;
|
||||
`;
|
||||
const result = await db.query(q, [projectId]);
|
||||
|
||||
for (const item of result.rows) {
|
||||
const endDate = moment(item.end_date);
|
||||
const completedDate = moment(item.completed_at);
|
||||
const overdueDays = completedDate.diff(endDate, "days");
|
||||
|
||||
if (overdueDays > 0) {
|
||||
item.overdue_days = overdueDays.toString();
|
||||
} else {
|
||||
item.overdue_days = "0";
|
||||
}
|
||||
|
||||
item.total_minutes_spent = Math.ceil(item.total_seconds_spent / 60);
|
||||
|
||||
if (~~(item.total_minutes_spent) > ~~(item.total_minutes)) {
|
||||
const overlogged_time = ~~(item.total_minutes_spent) - ~~(item.total_minutes);
|
||||
item.overlogged_time_string = formatDuration(moment.duration(overlogged_time, "minutes"));
|
||||
} else {
|
||||
item.overlogged_time_string = `0h 0m`;
|
||||
}
|
||||
}
|
||||
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
protected static getDateRangeClause(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");
|
||||
let query = `AND task_work_log.created_at::DATE >= '${start}'::DATE AND task_work_log.created_at < '${end}'::DATE + INTERVAL '1 day'`;
|
||||
|
||||
if (start === end) {
|
||||
query = `AND task_work_log.created_at::DATE = '${start}'::DATE`;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
if (key === DATE_RANGES.YESTERDAY)
|
||||
return "AND task_work_log.created_at >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE";
|
||||
if (key === DATE_RANGES.LAST_WEEK)
|
||||
return "AND task_work_log.created_at >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'";
|
||||
if (key === DATE_RANGES.LAST_MONTH)
|
||||
return "AND task_work_log.created_at >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'";
|
||||
if (key === DATE_RANGES.LAST_QUARTER)
|
||||
return "AND task_work_log.created_at >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'";
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
protected static formatEndDate(endDate: string) {
|
||||
const end = moment(endDate).format("YYYY-MM-DD");
|
||||
const fEndDate = moment(end);
|
||||
return fEndDate;
|
||||
}
|
||||
|
||||
protected static formatCurrentDate() {
|
||||
const current = moment().format("YYYY-MM-DD");
|
||||
const fCurrentDate = moment(current);
|
||||
return fCurrentDate;
|
||||
}
|
||||
|
||||
protected static getDaysLeft(endDate: string): number | null {
|
||||
if (!endDate) return null;
|
||||
|
||||
const fCurrentDate = this.formatCurrentDate();
|
||||
const fEndDate = this.formatEndDate(endDate);
|
||||
|
||||
return fEndDate.diff(fCurrentDate, "days");
|
||||
}
|
||||
|
||||
protected static isOverdue(endDate: string): boolean {
|
||||
if (!endDate) return false;
|
||||
|
||||
const fCurrentDate = this.formatCurrentDate();
|
||||
const fEndDate = this.formatEndDate(endDate);
|
||||
|
||||
return fEndDate.isBefore(fCurrentDate);
|
||||
}
|
||||
|
||||
protected static isToday(endDate: string): boolean {
|
||||
if (!endDate) return false;
|
||||
|
||||
const fCurrentDate = this.formatCurrentDate();
|
||||
const fEndDate = this.formatEndDate(endDate);
|
||||
|
||||
return fEndDate.isSame(fCurrentDate);
|
||||
}
|
||||
|
||||
|
||||
public static async getProjectsByTeam(
|
||||
teamId: string,
|
||||
size: string | number | null,
|
||||
offset: string | number | null,
|
||||
searchQuery: string | null,
|
||||
sortField: string,
|
||||
sortOrder: string,
|
||||
statusClause: string,
|
||||
healthClause: string,
|
||||
categoryClause: string,
|
||||
archivedClause = "",
|
||||
teamFilterClause: string,
|
||||
projectManagersClause: string) {
|
||||
|
||||
const q = `SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT p.id,
|
||||
p.name,
|
||||
p.color_code,
|
||||
|
||||
p.health_id AS project_health,
|
||||
(SELECT color_code
|
||||
FROM sys_project_healths
|
||||
WHERE sys_project_healths.id = p.health_id) AS health_color,
|
||||
|
||||
pc.id AS category_id,
|
||||
pc.name AS category_name,
|
||||
pc.color_code AS category_color,
|
||||
|
||||
(SELECT name FROM clients WHERE id = p.client_id) AS client,
|
||||
|
||||
p.team_id,
|
||||
(SELECT name FROM teams WHERE id = p.team_id) AS team_name,
|
||||
|
||||
ps.id AS status_id,
|
||||
ps.name AS status_name,
|
||||
ps.color_code AS status_color,
|
||||
ps.icon AS status_icon,
|
||||
|
||||
start_date,
|
||||
end_date,
|
||||
|
||||
(SELECT COALESCE(ROW_TO_JSON(pm), '{}'::JSON)
|
||||
FROM (SELECT team_member_id AS id,
|
||||
(SELECT COALESCE(ROW_TO_JSON(pmi), '{}'::JSON)
|
||||
FROM (SELECT name,
|
||||
email,
|
||||
avatar_url
|
||||
FROM team_member_info_view tmiv
|
||||
WHERE tmiv.team_member_id = pm.team_member_id
|
||||
AND tmiv.team_id = (SELECT team_id FROM projects WHERE id = p.id)) pmi) AS project_manager_info,
|
||||
EXISTS(SELECT email
|
||||
FROM email_invitations
|
||||
WHERE team_member_id = pm.team_member_id
|
||||
AND email_invitations.team_id = (SELECT team_id
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_id = pm.team_member_id)) AS pending_invitation,
|
||||
(SELECT active FROM team_members WHERE id = pm.team_member_id)
|
||||
FROM project_members pm
|
||||
WHERE project_id =p.id
|
||||
AND project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')) pm) AS project_manager,
|
||||
|
||||
(SELECT COALESCE(SUM(total_minutes), 0)
|
||||
FROM tasks
|
||||
WHERE project_id = p.id) AS estimated_time,
|
||||
|
||||
(SELECT SUM((SELECT COALESCE(SUM(time_spent), 0)
|
||||
FROM task_work_log
|
||||
WHERE task_id = tasks.id))
|
||||
FROM tasks
|
||||
WHERE project_id = p.id) AS actual_time,
|
||||
|
||||
(SELECT ROW_TO_JSON(rec)
|
||||
FROM (SELECT COUNT(ta.id) AS total,
|
||||
COUNT(CASE WHEN is_completed(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS done,
|
||||
COUNT(CASE WHEN is_doing(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS doing,
|
||||
COUNT(CASE WHEN is_todo(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS todo
|
||||
FROM tasks ta
|
||||
WHERE project_id = p.id) rec) AS tasks_stat,
|
||||
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT pu.content AS content,
|
||||
(SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
|
||||
FROM (SELECT u.name AS user_name,
|
||||
u.email AS user_email
|
||||
FROM project_comment_mentions pcm
|
||||
LEFT JOIN users u ON pcm.informed_by = u.id
|
||||
WHERE pcm.comment_id = pu.id) rec) AS mentions,
|
||||
pu.updated_at
|
||||
FROM project_comments pu
|
||||
WHERE pu.project_id = p.id
|
||||
ORDER BY pu.updated_at DESC
|
||||
LIMIT 1) AS rec) AS update,
|
||||
|
||||
(SELECT ROW_TO_JSON(rec)
|
||||
FROM (SELECT attribute_type,
|
||||
log_type,
|
||||
-- new case,
|
||||
(CASE
|
||||
WHEN (attribute_type = 'status')
|
||||
THEN (SELECT name FROM task_statuses WHERE id = old_value::UUID)
|
||||
WHEN (attribute_type = 'priority')
|
||||
THEN (SELECT name FROM task_priorities WHERE id = old_value::UUID)
|
||||
ELSE (old_value) END) AS previous,
|
||||
|
||||
-- new case
|
||||
(CASE
|
||||
WHEN (attribute_type = 'assignee')
|
||||
THEN (SELECT name FROM users WHERE id = new_value::UUID)
|
||||
WHEN (attribute_type = 'label')
|
||||
THEN (SELECT name FROM team_labels WHERE id = new_value::UUID)
|
||||
WHEN (attribute_type = 'status')
|
||||
THEN (SELECT name FROM task_statuses WHERE id = new_value::UUID)
|
||||
WHEN (attribute_type = 'priority')
|
||||
THEN (SELECT name FROM task_priorities WHERE id = new_value::UUID)
|
||||
ELSE (new_value) END) AS current,
|
||||
(SELECT name
|
||||
FROM users
|
||||
WHERE id = (SELECT reporter_id FROM tasks WHERE id = tal.task_id)),
|
||||
(SELECT ROW_TO_JSON(rec)
|
||||
FROM (SELECT (SELECT name FROM users WHERE users.id = tal.user_id),
|
||||
(SELECT avatar_url FROM users WHERE users.id = tal.user_id)) rec) AS done_by,
|
||||
(CASE
|
||||
WHEN (attribute_type = 'assignee')
|
||||
THEN (SELECT ROW_TO_JSON(rec)
|
||||
FROM (SELECT (CASE
|
||||
WHEN (new_value IS NOT NULL)
|
||||
THEN (SELECT name FROM users WHERE users.id = new_value::UUID)
|
||||
ELSE (next_string) END) AS name,
|
||||
(SELECT avatar_url FROM users WHERE users.id = new_value::UUID)) rec)
|
||||
ELSE (NULL) END) AS assigned_user,
|
||||
(SELECT name FROM tasks WHERE tasks.id = tal.task_id)
|
||||
FROM task_activity_logs tal
|
||||
WHERE task_id IN (SELECT id FROM tasks t WHERE t.project_id = p.id)
|
||||
ORDER BY tal.created_at DESC
|
||||
LIMIT 1) rec) AS last_activity
|
||||
FROM projects p
|
||||
LEFT JOIN project_categories pc ON pc.id = p.category_id
|
||||
LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
|
||||
WHERE ${teamFilterClause} ${searchQuery} ${healthClause} ${statusClause} ${categoryClause} ${projectManagersClause} ${archivedClause}
|
||||
ORDER BY ${sortField} ${sortOrder}
|
||||
LIMIT $2 OFFSET $3) t) AS projects
|
||||
FROM projects p
|
||||
LEFT JOIN project_categories pc ON pc.id = p.category_id
|
||||
LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
|
||||
WHERE ${teamFilterClause} ${searchQuery} ${healthClause} ${statusClause} ${categoryClause} ${projectManagersClause} ${archivedClause};`;
|
||||
const result = await db.query(q, [teamId, size, offset]);
|
||||
const [data] = result.rows;
|
||||
|
||||
for (const project of data.projects) {
|
||||
if (project.project_manager) {
|
||||
project.project_manager.name = project.project_manager.project_manager_info.name;
|
||||
project.project_manager.avatar_url = project.project_manager.project_manager_info.avatar_url;
|
||||
project.project_manager.color_code = getColor(project.project_manager.name);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public static convertMinutesToHoursAndMinutes(totalMinutes: number) {
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
public static convertSecondsToHoursAndMinutes(seconds: number) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
public static async exportProjects(teamId: string) {
|
||||
const q = `SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT p.id,
|
||||
p.name,
|
||||
(SELECT name
|
||||
FROM sys_project_healths
|
||||
WHERE sys_project_healths.id = p.health_id) AS project_health,
|
||||
pc.name AS category_name,
|
||||
(SELECT name FROM clients WHERE id = p.client_id) AS client,
|
||||
(SELECT name FROM teams WHERE id = p.team_id) AS team_name,
|
||||
ps.name AS status_name,
|
||||
start_date,
|
||||
end_date,
|
||||
(SELECT COALESCE(SUM(total_minutes), 0)
|
||||
FROM tasks
|
||||
WHERE project_id = p.id) AS estimated_time,
|
||||
(SELECT SUM((SELECT COALESCE(SUM(time_spent), 0)
|
||||
FROM task_work_log
|
||||
WHERE task_id = tasks.id))
|
||||
FROM tasks
|
||||
WHERE project_id = p.id) AS actual_time,
|
||||
(SELECT ROW_TO_JSON(rec)
|
||||
FROM (SELECT COUNT(ta.id) AS total,
|
||||
COUNT(CASE WHEN is_completed(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS done,
|
||||
COUNT(CASE WHEN is_doing(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS doing,
|
||||
COUNT(CASE WHEN is_todo(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS todo
|
||||
FROM tasks ta
|
||||
WHERE project_id = p.id) rec) AS tasks_stat,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT pu.content AS content,
|
||||
(SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
|
||||
FROM (SELECT u.name AS user_name,
|
||||
u.email AS user_email
|
||||
FROM project_comment_mentions pcm
|
||||
LEFT JOIN users u ON pcm.informed_by = u.id
|
||||
WHERE pcm.comment_id = pu.id) rec) AS mentions,
|
||||
pu.updated_at
|
||||
FROM project_comments pu
|
||||
WHERE pu.project_id = p.id
|
||||
ORDER BY pu.updated_at DESC
|
||||
LIMIT 1) AS rec) AS update,
|
||||
(SELECT ROW_TO_JSON(rec)
|
||||
FROM (SELECT attribute_type,
|
||||
log_type,
|
||||
-- new case,
|
||||
(CASE
|
||||
WHEN (attribute_type = 'status')
|
||||
THEN (SELECT name FROM task_statuses WHERE id = old_value::UUID)
|
||||
WHEN (attribute_type = 'priority')
|
||||
THEN (SELECT name FROM task_priorities WHERE id = old_value::UUID)
|
||||
ELSE (old_value) END) AS previous,
|
||||
|
||||
-- new case
|
||||
(CASE
|
||||
WHEN (attribute_type = 'assignee')
|
||||
THEN (SELECT name FROM users WHERE id = new_value::UUID)
|
||||
WHEN (attribute_type = 'label')
|
||||
THEN (SELECT name FROM team_labels WHERE id = new_value::UUID)
|
||||
WHEN (attribute_type = 'status')
|
||||
THEN (SELECT name FROM task_statuses WHERE id = new_value::UUID)
|
||||
WHEN (attribute_type = 'priority')
|
||||
THEN (SELECT name FROM task_priorities WHERE id = new_value::UUID)
|
||||
ELSE (new_value) END) AS current,
|
||||
(SELECT name
|
||||
FROM users
|
||||
WHERE id = (SELECT reporter_id FROM tasks WHERE id = tal.task_id)),
|
||||
(SELECT ROW_TO_JSON(rec)
|
||||
FROM (SELECT (SELECT name FROM users WHERE users.id = tal.user_id),
|
||||
(SELECT avatar_url FROM users WHERE users.id = tal.user_id)) rec) AS done_by,
|
||||
(CASE
|
||||
WHEN (attribute_type = 'assignee')
|
||||
THEN (SELECT ROW_TO_JSON(rec)
|
||||
FROM (SELECT (CASE
|
||||
WHEN (new_value IS NOT NULL)
|
||||
THEN (SELECT name FROM users WHERE users.id = new_value::UUID)
|
||||
ELSE (next_string) END) AS name,
|
||||
(SELECT avatar_url FROM users WHERE users.id = new_value::UUID)) rec)
|
||||
ELSE (NULL) END) AS assigned_user,
|
||||
(SELECT name FROM tasks WHERE tasks.id = tal.task_id)
|
||||
FROM task_activity_logs tal
|
||||
WHERE task_id IN (SELECT id FROM tasks t WHERE t.project_id = p.id)
|
||||
ORDER BY tal.created_at
|
||||
LIMIT 1) rec) AS last_activity
|
||||
FROM projects p
|
||||
LEFT JOIN project_categories pc ON pc.id = p.category_id
|
||||
LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
|
||||
WHERE p.team_id = $1 ORDER BY p.name) t) AS projects
|
||||
FROM projects p
|
||||
LEFT JOIN project_categories pc ON pc.id = p.category_id
|
||||
LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
|
||||
WHERE p.team_id = $1;`;
|
||||
|
||||
const result = await db.query(q, [teamId]);
|
||||
|
||||
const [data] = result.rows;
|
||||
|
||||
for (const project of data.projects) {
|
||||
project.team_color = getColor(project.team_name) + TASK_PRIORITY_COLOR_ALPHA;
|
||||
project.days_left = this.getDaysLeft(project.end_date);
|
||||
project.is_overdue = this.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 = this.convertMinutesToHoursAndMinutes(int(project.estimated_time));
|
||||
project.actual_time = 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[0];
|
||||
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, ` @${update.mentions[index].user_name} `);
|
||||
}
|
||||
});
|
||||
}
|
||||
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 data;
|
||||
}
|
||||
|
||||
public static async exportProjectsAll(teamId: string) {
|
||||
const q = `SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT p.id,
|
||||
p.name,
|
||||
(SELECT name
|
||||
FROM sys_project_healths
|
||||
WHERE sys_project_healths.id = p.health_id) AS project_health,
|
||||
pc.name AS category_name,
|
||||
(SELECT name FROM clients WHERE id = p.client_id) AS client,
|
||||
(SELECT name FROM teams WHERE id = p.team_id) AS team_name,
|
||||
ps.name AS status_name,
|
||||
start_date,
|
||||
end_date,
|
||||
(SELECT COALESCE(SUM(total_minutes), 0)
|
||||
FROM tasks
|
||||
WHERE project_id = p.id) AS estimated_time,
|
||||
(SELECT SUM((SELECT COALESCE(SUM(time_spent), 0)
|
||||
FROM task_work_log
|
||||
WHERE task_id = tasks.id))
|
||||
FROM tasks
|
||||
WHERE project_id = p.id) AS actual_time,
|
||||
(SELECT ROW_TO_JSON(rec)
|
||||
FROM (SELECT COUNT(ta.id) AS total,
|
||||
COUNT(CASE WHEN is_completed(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS done,
|
||||
COUNT(CASE WHEN is_doing(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS doing,
|
||||
COUNT(CASE WHEN is_todo(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS todo
|
||||
FROM tasks ta
|
||||
WHERE project_id = p.id) rec) AS tasks_stat,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT pu.content AS content,
|
||||
(SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
|
||||
FROM (SELECT u.name AS user_name,
|
||||
u.email AS user_email
|
||||
FROM project_comment_mentions pcm
|
||||
LEFT JOIN users u ON pcm.informed_by = u.id
|
||||
WHERE pcm.comment_id = pu.id) rec) AS mentions,
|
||||
pu.updated_at
|
||||
FROM project_comments pu
|
||||
WHERE pu.project_id = p.id
|
||||
ORDER BY pu.updated_at DESC
|
||||
LIMIT 1) AS rec) AS update,
|
||||
(SELECT ROW_TO_JSON(rec)
|
||||
FROM (SELECT attribute_type,
|
||||
log_type,
|
||||
-- new case,
|
||||
(CASE
|
||||
WHEN (attribute_type = 'status')
|
||||
THEN (SELECT name FROM task_statuses WHERE id = old_value::UUID)
|
||||
WHEN (attribute_type = 'priority')
|
||||
THEN (SELECT name FROM task_priorities WHERE id = old_value::UUID)
|
||||
ELSE (old_value) END) AS previous,
|
||||
|
||||
-- new case
|
||||
(CASE
|
||||
WHEN (attribute_type = 'assignee')
|
||||
THEN (SELECT name FROM users WHERE id = new_value::UUID)
|
||||
WHEN (attribute_type = 'label')
|
||||
THEN (SELECT name FROM team_labels WHERE id = new_value::UUID)
|
||||
WHEN (attribute_type = 'status')
|
||||
THEN (SELECT name FROM task_statuses WHERE id = new_value::UUID)
|
||||
WHEN (attribute_type = 'priority')
|
||||
THEN (SELECT name FROM task_priorities WHERE id = new_value::UUID)
|
||||
ELSE (new_value) END) AS current,
|
||||
(SELECT name
|
||||
FROM users
|
||||
WHERE id = (SELECT reporter_id FROM tasks WHERE id = tal.task_id)),
|
||||
(SELECT ROW_TO_JSON(rec)
|
||||
FROM (SELECT (SELECT name FROM users WHERE users.id = tal.user_id),
|
||||
(SELECT avatar_url FROM users WHERE users.id = tal.user_id)) rec) AS done_by,
|
||||
(CASE
|
||||
WHEN (attribute_type = 'assignee')
|
||||
THEN (SELECT ROW_TO_JSON(rec)
|
||||
FROM (SELECT (CASE
|
||||
WHEN (new_value IS NOT NULL)
|
||||
THEN (SELECT name FROM users WHERE users.id = new_value::UUID)
|
||||
ELSE (next_string) END) AS name,
|
||||
(SELECT avatar_url FROM users WHERE users.id = new_value::UUID)) rec)
|
||||
ELSE (NULL) END) AS assigned_user,
|
||||
(SELECT name FROM tasks WHERE tasks.id = tal.task_id)
|
||||
FROM task_activity_logs tal
|
||||
WHERE task_id IN (SELECT id FROM tasks t WHERE t.project_id = p.id)
|
||||
ORDER BY tal.created_at
|
||||
LIMIT 1) rec) AS last_activity
|
||||
FROM projects p
|
||||
LEFT JOIN project_categories pc ON pc.id = p.category_id
|
||||
LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
|
||||
WHERE in_organization(p.team_id, $1) ORDER BY p.name) t) AS projects
|
||||
FROM projects p
|
||||
LEFT JOIN project_categories pc ON pc.id = p.category_id
|
||||
LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
|
||||
WHERE in_organization(p.team_id, $1);`;
|
||||
|
||||
const result = await db.query(q, [teamId]);
|
||||
|
||||
const [data] = result.rows;
|
||||
|
||||
for (const project of data.projects) {
|
||||
project.team_color = getColor(project.team_name) + TASK_PRIORITY_COLOR_ALPHA;
|
||||
project.days_left = this.getDaysLeft(project.end_date);
|
||||
project.is_overdue = this.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 = this.convertMinutesToHoursAndMinutes(int(project.estimated_time));
|
||||
project.actual_time = 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[0];
|
||||
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, ` @${update.mentions[index].user_name} `);
|
||||
}
|
||||
});
|
||||
}
|
||||
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 data;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import ReportingControllerBase from "./reporting-controller-base";
|
||||
import HandleExceptions from "../../decorators/handle-exceptions";
|
||||
import {IWorkLenzRequest} from "../../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../../interfaces/worklenz-response";
|
||||
import db from "../../config/db";
|
||||
import {ServerResponse} from "../../models/server-response";
|
||||
|
||||
export default class ReportingInfoController extends ReportingControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getInfo(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT organization_name
|
||||
FROM organizations
|
||||
WHERE user_id = (SELECT user_id FROM teams WHERE id = $1);
|
||||
`;
|
||||
const result = await db.query(q, [this.getCurrentTeamId(req)]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
import moment from "moment";
|
||||
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {getDatesForResourceAllocation, getWeekRange} from "../shared/tasks-controller-utils";
|
||||
import {getColor} from "../shared/utils";
|
||||
|
||||
|
||||
export default class ResourceallocationController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getProjectWiseResources(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {start, end} = req.query;
|
||||
|
||||
const dates = await getDatesForResourceAllocation(start as string, end as string);
|
||||
const months = await getWeekRange(dates);
|
||||
|
||||
const q = `SELECT get_project_wise_resources($1, $2, $3) as resources;`;
|
||||
const result = await db.query(q, [start, moment(dates.length && dates.at(-1)?.date).format("YYYY-MM-DD") || end, req.user?.team_id || null]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const scheduleData = JSON.parse(data.resources);
|
||||
|
||||
for (const element of scheduleData) {
|
||||
for (const schedule of element.schedule) {
|
||||
const min = dates.findIndex((date) => moment(schedule.date_series).isSame(date.date, "days")) || 0;
|
||||
schedule.min = min + 1;
|
||||
}
|
||||
|
||||
for (const task of element.unassigned_tasks) {
|
||||
const min = dates.findIndex((date) => moment(task.date_series).isSame(date.date, "days")) || 0;
|
||||
task.min = min + 1;
|
||||
}
|
||||
|
||||
for (const member of element.project_members) {
|
||||
for (const task of member.tasks) {
|
||||
const min = dates.findIndex((date) => moment(task.date_series).isSame(date.date, "days")) || 0;
|
||||
task.min = min + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, {projects: scheduleData, dates, months}));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getUserWiseResources(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {start, end} = req.query;
|
||||
|
||||
const dates = await getDatesForResourceAllocation(start as string, end as string);
|
||||
const months = await getWeekRange(dates);
|
||||
|
||||
const q = `SELECT get_team_wise_resources($1, $2, $3) as resources;`;
|
||||
const result = await db.query(q, [start, moment(dates.length && dates.at(-1)?.date).format("YYYY-MM-DD") || end, req.user?.team_id || null]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const scheduleData = JSON.parse(data.resources);
|
||||
|
||||
const obj = [];
|
||||
|
||||
for (const element of scheduleData) {
|
||||
element.color_code = getColor(element.name);
|
||||
for (const schedule of element.schedule) {
|
||||
const min = dates.findIndex((date) => moment(schedule.date_series).isSame(date.date, "days")) || 0;
|
||||
schedule.min = min + 1;
|
||||
}
|
||||
|
||||
for (const member of element.project_members) {
|
||||
for (const task of member.tasks) {
|
||||
const min = dates.findIndex((date) => moment(task.date_series).isSame(date.date, "days")) || 0;
|
||||
task.min = min + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, {projects: scheduleData, dates, months}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA } from "../../shared/constants";
|
||||
import { getColor } from "../../shared/utils";
|
||||
import WorklenzControllerBase from ".././worklenz-controller-base";
|
||||
|
||||
export const GroupBy = {
|
||||
STATUS: "status",
|
||||
PRIORITY: "priority",
|
||||
LABELS: "labels",
|
||||
PHASE: "phase"
|
||||
};
|
||||
|
||||
export interface IScheduleTaskGroup {
|
||||
id?: string;
|
||||
name: string;
|
||||
color_code: string;
|
||||
category_id: string | null;
|
||||
old_category_id?: string;
|
||||
tasks: any[];
|
||||
isExpand: boolean;
|
||||
}
|
||||
|
||||
export default class ScheduleTasksControllerBase extends WorklenzControllerBase {
|
||||
protected static calculateTaskCompleteRatio(totalCompleted: number, totalTasks: number) {
|
||||
if (totalCompleted === 0 && totalTasks === 0) return 0;
|
||||
const ratio = ((totalCompleted / totalTasks) * 100);
|
||||
return ratio == Infinity ? 100 : ratio.toFixed();
|
||||
}
|
||||
|
||||
public static updateTaskViewModel(task: any) {
|
||||
task.progress = ~~(task.total_minutes_spent / task.total_minutes * 100);
|
||||
task.overdue = task.total_minutes < task.total_minutes_spent;
|
||||
|
||||
if (typeof task.sub_tasks_count === "undefined") task.sub_tasks_count = "0";
|
||||
|
||||
task.is_sub_task = !!task.parent_task_id;
|
||||
|
||||
task.name_color = getColor(task.name);
|
||||
task.priority_color = PriorityColorCodes[task.priority_value] || PriorityColorCodes["0"];
|
||||
task.show_sub_tasks = false;
|
||||
|
||||
if (task.phase_id) {
|
||||
task.phase_color = task.phase_name
|
||||
? getColor(task.phase_name) + TASK_PRIORITY_COLOR_ALPHA
|
||||
: null;
|
||||
}
|
||||
|
||||
if (Array.isArray(task.assignees)) {
|
||||
for (const assignee of task.assignees) {
|
||||
assignee.color_code = getColor(assignee.name);
|
||||
}
|
||||
}
|
||||
|
||||
task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
|
||||
task.priority_color = task.priority_color + TASK_PRIORITY_COLOR_ALPHA;
|
||||
|
||||
const totalCompleted = +task.completed_sub_tasks + +task.parent_task_completed;
|
||||
const totalTasks = +task.sub_tasks_count + 1; // +1 for parent
|
||||
task.complete_ratio = ScheduleTasksControllerBase.calculateTaskCompleteRatio(totalCompleted, totalTasks);
|
||||
task.completed_count = totalCompleted;
|
||||
task.total_tasks_count = totalTasks;
|
||||
|
||||
return task;
|
||||
}
|
||||
}
|
||||
945
worklenz-backend/src/controllers/schedule/schedule-controller.ts
Normal file
945
worklenz-backend/src/controllers/schedule/schedule-controller.ts
Normal file
@@ -0,0 +1,945 @@
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import {createHmac} from "crypto";
|
||||
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class SharedprojectsController extends WorklenzControllerBase {
|
||||
private static getShareLink(hash: string) {
|
||||
return `https://${process.env.HOSTNAME}/share/${hash}`;
|
||||
}
|
||||
|
||||
private static createShareInfo(name?: string, createdAt?: string, hash?: string) {
|
||||
if (!name || !createdAt || !hash) return null;
|
||||
return {
|
||||
url: this.getShareLink(hash),
|
||||
created_by: name?.split(" ")[0],
|
||||
created_at: createdAt
|
||||
};
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
INSERT INTO shared_projects (project_id, team_id, enabled_by, hash_value)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, created_at;
|
||||
`;
|
||||
|
||||
const hash = createHmac("sha256", req.body.project_id).digest("hex");
|
||||
|
||||
const result = await db.query(q, [req.body.project_id, req.user?.team_id, req.user?.id, hash]);
|
||||
const [data] = result.rows;
|
||||
if (!data?.id)
|
||||
return res.status(400).send(new ServerResponse(true, null));
|
||||
return res.status(200).send(new ServerResponse(true, this.createShareInfo(req.user?.name?.split(" ")[0], data.createdAt, hash)));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT hash_value, created_at, (SELECT name FROM users WHERE id = enabled_by)
|
||||
FROM shared_projects
|
||||
WHERE project_id = $1
|
||||
AND team_id = $2;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, this.createShareInfo(data?.name, data?.created_at, data?.hash_value)));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `DELETE FROM shared_projects WHERE project_id = $1 AND team_id = $2;`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
119
worklenz-backend/src/controllers/sub-tasks-controller.ts
Normal file
119
worklenz-backend/src/controllers/sub-tasks-controller.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import moment from "moment";
|
||||
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import {PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA} from "../shared/constants";
|
||||
import {getColor} from "../shared/utils";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class SubTasksController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getNames(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT name FROM tasks WHERE archived IS FALSE AND parent_task_id = $1;`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT t.id,
|
||||
t.name,
|
||||
t.description,
|
||||
t.project_id,
|
||||
t.parent_task_id,
|
||||
t.priority_id AS priority,
|
||||
tp.name AS priority_name,
|
||||
t.end_date,
|
||||
(ts.id) AS status,
|
||||
(ts.name) AS status_name,
|
||||
TRUE AS is_sub_task,
|
||||
(tsc.color_code) AS status_color,
|
||||
(SELECT name FROM projects WHERE id = t.project_id) AS project_name,
|
||||
(SELECT value FROM task_priorities WHERE id = t.priority_id) AS priority_value,
|
||||
total_minutes,
|
||||
(SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id) AS total_minutes_spent,
|
||||
(SELECT get_task_assignees(t.id)) AS assignees,
|
||||
(SELECT ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r)))
|
||||
FROM (SELECT task_labels.label_id AS id,
|
||||
(SELECT name FROM team_labels WHERE id = task_labels.label_id),
|
||||
(SELECT color_code FROM team_labels WHERE id = task_labels.label_id)
|
||||
FROM task_labels
|
||||
WHERE task_id = t.id
|
||||
ORDER BY name) r) AS labels,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(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
|
||||
FROM tasks t
|
||||
INNER JOIN task_statuses ts ON ts.id = t.status_id
|
||||
INNER JOIN task_priorities tp ON tp.id = t.priority_id
|
||||
LEFT JOIN sys_task_status_categories tsc ON ts.category_id = tsc.id
|
||||
WHERE parent_task_id = $1
|
||||
ORDER BY created_at;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
|
||||
for (const task of result.rows) {
|
||||
task.priority_color = PriorityColorCodes[task.priority_value] || null;
|
||||
|
||||
task.time_spent = {hours: Math.floor(task.total_minutes_spent / 60), minutes: task.total_minutes_spent % 60};
|
||||
task.time_spent_string = `${task.time_spent.hours}h ${task.time_spent.minutes}m`;
|
||||
task.total_time_string = `${Math.floor(task.total_minutes / 60)}h ${task.total_minutes % 60}m`;
|
||||
|
||||
task.assignees.map((a: any) => a.color_code = getColor(a.name));
|
||||
task.names = this.createTagList(task.assignees);
|
||||
task.labels = this.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 res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getSubTasksRoadMap(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const dates = req.body;
|
||||
const q = `
|
||||
SELECT tasks.id,
|
||||
tasks.name,
|
||||
tasks.start_date,
|
||||
tasks.end_date,
|
||||
tp.name AS priority,
|
||||
tasks.end_date,
|
||||
(SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = tasks.status_id)) AS status_color,
|
||||
(SELECT get_task_assignees(tasks.id)) AS assignees
|
||||
FROM tasks
|
||||
INNER JOIN task_statuses ts ON ts.task_id = tasks.id
|
||||
INNER JOIN task_priorities tp ON tp.id = tasks.priority_id
|
||||
WHERE archived IS FALSE AND parent_task_id = $1
|
||||
ORDER BY created_at DESC;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
|
||||
const maxInlineNames = 4;
|
||||
for (const task of result.rows) {
|
||||
task.assignees.map((a: any) => a.color_code = getColor(a.name));
|
||||
task.names = this.createTagList(task.assignees);
|
||||
|
||||
if (task?.assignees.length <= maxInlineNames) {
|
||||
const min: number = dates.findIndex((date: any) => moment(task.start_date).isSame(date.date, "days"));
|
||||
const max: number = dates.findIndex((date: any) => moment(task.end_date).isSame(date.date, "days"));
|
||||
task.min = min + 1;
|
||||
task.max = max > 0 ? max + 2 : max;
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
265
worklenz-backend/src/controllers/task-comments-controller.ts
Normal file
265
worklenz-backend/src/controllers/task-comments-controller.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import { NotificationsService } from "../services/notifications/notifications.service";
|
||||
import { log_error } from "../shared/utils";
|
||||
import { HTML_TAG_REGEXP } from "../shared/constants";
|
||||
import { getBaseUrl } from "../cron_jobs/helpers";
|
||||
import { ICommentEmailNotification } from "../interfaces/comment-email-notification";
|
||||
import { sendTaskComment } from "../shared/email-notifications";
|
||||
|
||||
interface ITaskAssignee {
|
||||
team_member_id: string;
|
||||
project_member_id: string;
|
||||
name: string;
|
||||
email_notifications_enabled: string;
|
||||
avatar_url: string;
|
||||
user_id: string;
|
||||
email: string;
|
||||
socket_id: string;
|
||||
team_id: string;
|
||||
user_name: string;
|
||||
}
|
||||
|
||||
interface IMailConfig {
|
||||
message: string;
|
||||
receiverEmail: string;
|
||||
receiverName: string;
|
||||
content: string;
|
||||
commentId: string;
|
||||
projectId: string;
|
||||
taskId: string;
|
||||
teamName: string;
|
||||
projectName: string;
|
||||
taskName: string;
|
||||
}
|
||||
|
||||
interface IMention {
|
||||
team_member_id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
async function getAssignees(taskId: string): Promise<Array<ITaskAssignee>> {
|
||||
const result1 = await db.query("SELECT get_task_assignees($1) AS assignees;", [taskId]);
|
||||
const [d] = result1.rows;
|
||||
return d.assignees || [];
|
||||
}
|
||||
|
||||
export default class TaskCommentsController extends WorklenzControllerBase {
|
||||
|
||||
private static replaceContent(messageContent: string, mentions: IMention[]) {
|
||||
const mentionNames = mentions.map(mention => mention.name);
|
||||
|
||||
const replacedContent = mentionNames.reduce(
|
||||
(content, mentionName, index) => {
|
||||
const regex = new RegExp(`@${mentionName}`, "g");
|
||||
return content.replace(regex, `{${index}}`);
|
||||
},
|
||||
messageContent
|
||||
);
|
||||
|
||||
return replacedContent;
|
||||
}
|
||||
|
||||
private static async getUserDataByTeamMemberId(senderUserId: string, teamMemberId: string, projectId: string) {
|
||||
const q = `
|
||||
SELECT id,
|
||||
socket_id,
|
||||
users.name AS user_name,
|
||||
(SELECT email_notifications_enabled
|
||||
FROM notification_settings
|
||||
WHERE notification_settings.team_id = (SELECT team_id FROM team_members WHERE id = $2)
|
||||
AND notification_settings.user_id = users.id),
|
||||
(SELECT name FROM teams WHERE id = (SELECT team_id FROM team_members WHERE id = $2)) AS team,
|
||||
(SELECT name FROM projects WHERE id = $3) AS project,
|
||||
(SELECT color_code FROM projects WHERE id = $3) AS project_color
|
||||
FROM users
|
||||
WHERE id != $1
|
||||
AND id IN (SELECT user_id FROM team_members WHERE id = $2);
|
||||
`;
|
||||
const result = await db.query(q, [senderUserId, teamMemberId, projectId]);
|
||||
const [data] = result.rows;
|
||||
return data;
|
||||
}
|
||||
|
||||
private static async updateComment(commentId: string, messageId: string) {
|
||||
if (!commentId || messageId) return;
|
||||
try {
|
||||
await db.query("UPDATE task_comments SET ses_message_id = $2 WHERE id = $1;", [commentId, messageId]);
|
||||
} catch (e) {
|
||||
log_error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
req.body.user_id = req.user?.id;
|
||||
req.body.team_id = req.user?.team_id;
|
||||
const {mentions} = req.body;
|
||||
|
||||
let commentContent = req.body.content;
|
||||
if (mentions.length > 0) {
|
||||
commentContent = await this.replaceContent(commentContent, mentions);
|
||||
}
|
||||
|
||||
req.body.content = commentContent;
|
||||
|
||||
const q = `SELECT create_task_comment($1) AS comment;`;
|
||||
const result = await db.query(q, [JSON.stringify(req.body)]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const response = data.comment;
|
||||
|
||||
const mentionMessage = `<b>${req.user?.name}</b> has mentioned you in a comment on <b>${response.task_name}</b> (${response.team_name})`;
|
||||
// const mentions = [...new Set(req.body.mentions || [])] as string[]; // remove duplicates
|
||||
|
||||
const assignees = await getAssignees(req.body.task_id);
|
||||
|
||||
const commentMessage = `<b>${req.user?.name}</b> added a comment on <b>${response.task_name}</b> (${response.team_name})`;
|
||||
for (const member of assignees || []) {
|
||||
if (member.user_id && member.user_id === req.user?.id) continue;
|
||||
|
||||
void NotificationsService.createNotification({
|
||||
userId: member.user_id,
|
||||
teamId: req.user?.team_id as string,
|
||||
socketId: member.socket_id,
|
||||
message: commentMessage,
|
||||
taskId: req.body.task_id,
|
||||
projectId: response.project_id
|
||||
});
|
||||
|
||||
if (member.email_notifications_enabled)
|
||||
await this.sendMail({
|
||||
message: commentMessage,
|
||||
receiverEmail: member.email,
|
||||
receiverName: member.name,
|
||||
content: req.body.content,
|
||||
commentId: response.id,
|
||||
projectId: response.project_id,
|
||||
taskId: req.body.task_id,
|
||||
teamName: response.team_name,
|
||||
projectName: response.project_name,
|
||||
taskName: response.task_name
|
||||
});
|
||||
}
|
||||
|
||||
const senderUserId = req.user?.id as string;
|
||||
|
||||
for (const mention of mentions) {
|
||||
if (mention) {
|
||||
const member = await this.getUserDataByTeamMemberId(senderUserId, mention.team_member_id, response.project_id);
|
||||
if (member) {
|
||||
|
||||
NotificationsService.sendNotification({
|
||||
team: member.team,
|
||||
receiver_socket_id: member.socket_id,
|
||||
message: mentionMessage,
|
||||
task_id: req.body.task_id,
|
||||
project_id: response.project_id,
|
||||
project: member.project,
|
||||
project_color: member.project_color,
|
||||
team_id: req.user?.team_id as string
|
||||
});
|
||||
|
||||
if (member.email_notifications_enabled)
|
||||
await this.sendMail({
|
||||
message: mentionMessage,
|
||||
receiverEmail: member.email,
|
||||
receiverName: member.user_name,
|
||||
content: req.body.content,
|
||||
commentId: response.id,
|
||||
projectId: response.project_id,
|
||||
taskId: req.body.task_id,
|
||||
teamName: response.team_name,
|
||||
projectName: response.project_name,
|
||||
taskName: response.task_name
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.comment));
|
||||
}
|
||||
|
||||
private static async sendMail(config: IMailConfig) {
|
||||
const subject = config.message.replace(HTML_TAG_REGEXP, "");
|
||||
const taskUrl = `${getBaseUrl()}/worklenz/projects/${config.projectId}?tab=tasks-list&task=${config.taskId}&focus=comments`;
|
||||
const settingsUrl = `${getBaseUrl()}/worklenz/settings/notifications`;
|
||||
|
||||
const data: ICommentEmailNotification = {
|
||||
greeting: `Hi ${config.receiverName}`,
|
||||
summary: subject,
|
||||
team: config.teamName,
|
||||
project_name: config.projectName,
|
||||
comment: config.content,
|
||||
task: config.taskName,
|
||||
settings_url: settingsUrl,
|
||||
task_url: taskUrl,
|
||||
};
|
||||
|
||||
const messageId = await sendTaskComment(config.receiverEmail, data);
|
||||
if (messageId) {
|
||||
void TaskCommentsController.updateComment(config.commentId, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getByTaskId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT task_comments.id,
|
||||
tc.text_content AS content,
|
||||
task_comments.user_id,
|
||||
task_comments.team_member_id,
|
||||
(SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id) AS member_name,
|
||||
u.avatar_url,
|
||||
task_comments.created_at,
|
||||
(SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
|
||||
FROM (SELECT tmiv.name AS user_name,
|
||||
tmiv.email AS user_email
|
||||
FROM task_comment_mentions tcm
|
||||
LEFT JOIN team_member_info_view tmiv ON tcm.informed_by = tmiv.team_member_id
|
||||
WHERE tcm.comment_id = task_comments.id) rec) AS mentions
|
||||
FROM task_comments
|
||||
INNER JOIN task_comment_contents tc ON task_comments.id = tc.comment_id
|
||||
INNER JOIN team_members tm ON task_comments.team_member_id = tm.id
|
||||
LEFT JOIN users u ON tm.user_id = u.id
|
||||
WHERE task_comments.task_id = $1
|
||||
ORDER BY task_comments.created_at DESC;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id]); // task id
|
||||
|
||||
for (const comment of result.rows) {
|
||||
comment.content = await comment.content.replace(/\n/g, "</br>");
|
||||
const {mentions} = comment;
|
||||
if (mentions.length > 0) {
|
||||
const placeHolders = comment.content.match(/{\d+}/g);
|
||||
if (placeHolders) {
|
||||
placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
|
||||
const index = parseInt(placeHolder.match(/\d+/)[0]);
|
||||
if (index >= 0 && index < comment.mentions.length) {
|
||||
comment.content = comment.content.replace(placeHolder, `<span class="mentions"> @${comment.mentions[index].user_name} </span>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `DELETE
|
||||
FROM task_comments
|
||||
WHERE id = $1
|
||||
AND task_id = $2
|
||||
AND user_id = $3;`;
|
||||
const result = await db.query(q, [req.params.id, req.params.taskId, req.user?.id || null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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";
|
||||
|
||||
export default class TaskListColumnsController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getProjectTaskListColumns(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT name,
|
||||
key,
|
||||
index,
|
||||
pinned,
|
||||
(SELECT phase_label FROM projects WHERE id = $1) AS phase_label
|
||||
FROM project_task_list_cols
|
||||
WHERE project_id = $1
|
||||
ORDER BY index;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
const phase = result.rows.find(phase => phase.key === "PHASE");
|
||||
if (phase)
|
||||
phase.name = phase.phase_label;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async toggleColumn(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `UPDATE project_task_list_cols
|
||||
SET pinned = $3
|
||||
WHERE project_id = $1
|
||||
AND key = $2;`;
|
||||
const result = await db.query(q, [req.params.id, req.body.key, !!req.body.pinned]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
119
worklenz-backend/src/controllers/task-phases-controller.ts
Normal file
119
worklenz-backend/src/controllers/task-phases-controller.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {getColor} from "../shared/utils";
|
||||
import {TASK_STATUS_COLOR_ALPHA} from "../shared/constants";
|
||||
|
||||
export default class TaskPhasesController 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 project_phases (name, color_code, project_id, sort_index)
|
||||
VALUES (
|
||||
CONCAT('Untitled Phase (', (SELECT COUNT(*) FROM project_phases WHERE project_id = $2) + 1, ')'),
|
||||
$1,
|
||||
$2,
|
||||
(SELECT COUNT(*) FROM project_phases WHERE project_id = $2) + 1)
|
||||
RETURNING id, name, color_code, sort_index;
|
||||
`;
|
||||
|
||||
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 = getColor(data.name) + 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 task_phase WHERE phase_id = project_phases.id) AS usage
|
||||
FROM project_phases
|
||||
WHERE project_id = $1
|
||||
ORDER BY sort_index 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()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
UPDATE project_phases
|
||||
SET name = $3
|
||||
WHERE id = $1
|
||||
AND project_id = $2
|
||||
RETURNING id, name, color_code;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [req.params.id, req.query.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 project_phases SET color_code = $3 WHERE id = $1 AND project_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 updateLabel(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
UPDATE projects
|
||||
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 updateSortOrder (req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const body = {
|
||||
phases: req.body.phases.reverse(),
|
||||
project_id: req.body.project_id
|
||||
};
|
||||
|
||||
const q = `SELECT handle_phase_sort_order($1);`;
|
||||
const result = await db.query(q, [JSON.stringify(body)]);
|
||||
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 project_phases
|
||||
WHERE id = $1
|
||||
AND project_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,28 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {PriorityColorCodes} from "../shared/constants";
|
||||
|
||||
export default class TaskPrioritiesController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async get(_req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name, value From task_priorities ORDER BY value;`;
|
||||
const result = await db.query(q, []);
|
||||
for (const item of result.rows)
|
||||
item.color_code = PriorityColorCodes[item.value] || PriorityColorCodes["0"];
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getById(_req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name From priorities WHERE id=$1;`;
|
||||
const result = await db.query(q, [_req.params.id]);
|
||||
const data = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
}
|
||||
161
worklenz-backend/src/controllers/task-statuses-controller.ts
Normal file
161
worklenz-backend/src/controllers/task-statuses-controller.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
const existsErrorMessage = "At least one status should exists under each category.";
|
||||
|
||||
export default class TaskStatusesController extends WorklenzControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
INSERT INTO task_statuses (name, project_id, team_id, category_id, sort_order)
|
||||
VALUES ($1, $2, $3, $4, (SELECT MAX(sort_order) FROM task_statuses WHERE project_id = $2) + 1);
|
||||
`;
|
||||
const result = await db.query(q, [req.body.name, req.body.project_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_task_status($1, $2)`;
|
||||
const result = await db.query(q, [JSON.stringify(req.body), team_id]);
|
||||
const data = result.rows[0].create_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.project)
|
||||
return res.status(400).send(new ServerResponse(false, null));
|
||||
|
||||
const q = `
|
||||
SELECT task_statuses.id,
|
||||
task_statuses.name,
|
||||
stsc.color_code,
|
||||
stsc.name AS category_name,
|
||||
task_statuses.category_id,
|
||||
stsc.description
|
||||
FROM task_statuses
|
||||
INNER JOIN sys_task_status_categories stsc ON task_statuses.category_id = stsc.id
|
||||
WHERE project_id = $1
|
||||
AND team_id = $2
|
||||
ORDER BY task_statuses.sort_order;
|
||||
`;
|
||||
const result = await db.query(q, [req.query.project, 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));
|
||||
}
|
||||
|
||||
private static async getStatusByGroups(projectId: string) {
|
||||
if (!projectId) return;
|
||||
|
||||
const q = ``;
|
||||
const result = await db.query(q, [projectId]);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
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 task_statuses.id = $1
|
||||
AND project_id = $2;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.query.project_id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
private static async hasMoreCategories(statusId: string, projectId: string) {
|
||||
if (!statusId || !projectId)
|
||||
return false;
|
||||
|
||||
const q = `
|
||||
SELECT COUNT(*) AS count
|
||||
FROM task_statuses
|
||||
WHERE category_id = (SELECT category_id FROM task_statuses WHERE id = $1)
|
||||
AND project_id = $2;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [statusId, projectId]);
|
||||
const [data] = result.rows;
|
||||
return +data.count >= 2;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const hasMoreCategories = await TaskStatusesController.hasMoreCategories(req.params.id, req.body.project_id);
|
||||
|
||||
if (!hasMoreCategories)
|
||||
return res.status(200).send(new ServerResponse(false, null, existsErrorMessage).withTitle("Status update failed!"));
|
||||
|
||||
const q = `
|
||||
UPDATE 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 project_id = $3
|
||||
RETURNING (SELECT color_code FROM sys_task_status_categories WHERE id = task_statuses.category_id);
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.body.name, req.body.project_id, req.body.category_id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateName(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
UPDATE task_statuses
|
||||
SET name = $2
|
||||
WHERE id = $1
|
||||
AND project_id = $3
|
||||
RETURNING (SELECT color_code FROM sys_task_status_categories WHERE id = task_statuses.category_id);
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.body.name, req.body.project_id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateStatusOrder(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT update_status_order($1);`;
|
||||
const result = await db.query(q, [JSON.stringify(req.body.status_order)]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions({
|
||||
raisedExceptions: {
|
||||
"ERROR_ONE_SHOULD_EXISTS": existsErrorMessage
|
||||
}
|
||||
})
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT move_tasks_and_delete_status($1)`;
|
||||
|
||||
const body = {
|
||||
id: req.params.id,
|
||||
project_id: req.query.project,
|
||||
replacing_status: req.query.replace
|
||||
};
|
||||
|
||||
const result = await db.query(q, [JSON.stringify(body)]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class TasktemplatesController extends WorklenzControllerBase {
|
||||
@HandleExceptions({
|
||||
raisedExceptions: {
|
||||
"TASK_TEMPLATE_EXISTS_ERROR": `A template with the name "{0}" already exists. Please choose a different name.`
|
||||
}
|
||||
})
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {name, tasks} = req.body;
|
||||
const q = `SELECT create_task_template($1, $2, $3);`;
|
||||
const result = await db.query(q, [name.trim(), req.user?.team_id, JSON.stringify(tasks)]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data, "Task template created successfully"));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name, created_at FROM task_templates WHERE team_id = $1 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 getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {id} = req.params;
|
||||
const q = `SELECT id, name,
|
||||
((SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
|
||||
FROM (SELECT task_templates_tasks.name AS name,
|
||||
task_templates_tasks.total_minutes AS total_minutes
|
||||
FROM task_templates_tasks
|
||||
WHERE template_id = task_templates.id) rec)) AS tasks
|
||||
FROM task_templates
|
||||
WHERE id = $1
|
||||
ORDER BY name`;
|
||||
const result = await db.query(q, [id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions({
|
||||
raisedExceptions: {
|
||||
"TASK_TEMPLATE_EXISTS_ERROR": `A template with the name "{0}" already exists. Please choose a different name.`
|
||||
}
|
||||
})
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {name, tasks} = req.body;
|
||||
const {id} = req.params;
|
||||
|
||||
const q = `SELECT update_task_template($1, $2, $3, $4);`;
|
||||
const result = await db.query(q, [id, name, JSON.stringify(tasks), req.user?.team_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows, "Template updated."));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {id} = req.params;
|
||||
|
||||
const q = `DELETE FROM task_templates WHERE id = $1;`;
|
||||
const result = await db.query(q, [id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows, "Template deleted."));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async import(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {id} = req.params;
|
||||
|
||||
const q = `SELECT import_tasks_from_template($1, $2, $3);`;
|
||||
const result = await db.query(q, [id, req.user?.id, JSON.stringify(req.body)]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data, "Tasks imported successfully!"));
|
||||
}
|
||||
}
|
||||
237
worklenz-backend/src/controllers/task-work-log-controller.ts
Normal file
237
worklenz-backend/src/controllers/task-work-log-controller.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import Excel from "exceljs";
|
||||
import moment from "moment";
|
||||
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {formatDuration, getColor, log_error, toSeconds} from "../shared/utils";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import momentTime from "moment-timezone";
|
||||
|
||||
export default class TaskWorklogController extends WorklenzControllerBase {
|
||||
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {id, seconds_spent, description, created_at, formatted_start} = req.body;
|
||||
const q = `INSERT INTO task_work_log (time_spent, description, task_id, user_id, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5);`;
|
||||
const params = [seconds_spent, description, id, req.user?.id, formatted_start];
|
||||
const result = await db.query(q, params);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
private static async getTimeLogs(id: string, timeZone: string) {
|
||||
if (!id) return [];
|
||||
|
||||
const q = `
|
||||
WITH time_logs AS (
|
||||
--
|
||||
SELECT id,
|
||||
description,
|
||||
time_spent,
|
||||
created_at,
|
||||
user_id,
|
||||
logged_by_timer,
|
||||
(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
|
||||
FROM task_work_log
|
||||
WHERE task_id = $1
|
||||
--
|
||||
)
|
||||
SELECT id,
|
||||
time_spent,
|
||||
description,
|
||||
created_at,
|
||||
user_id,
|
||||
logged_by_timer,
|
||||
created_at AS start_time,
|
||||
(created_at + INTERVAL '1 second' * time_spent) AS end_time,
|
||||
user_name,
|
||||
user_email,
|
||||
avatar_url
|
||||
FROM time_logs
|
||||
ORDER BY created_at DESC;
|
||||
`;
|
||||
const result = await db.query(q, [id]);
|
||||
if (timeZone) {
|
||||
for (const res of result.rows) {
|
||||
res.start_time = momentTime.tz(res.start_time, `${timeZone}`).format();
|
||||
res.end_time = momentTime.tz(res.end_time, `${timeZone}`).format();
|
||||
}
|
||||
}
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getByTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const results = await this.getTimeLogs(req.params.id, req.query.time_zone_name as string);
|
||||
|
||||
for (const item of results)
|
||||
item.avatar_color = getColor(item.user_name);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, results));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {seconds_spent, description, created_at, formatted_start} = req.body;
|
||||
const q = `
|
||||
UPDATE task_work_log
|
||||
SET time_spent = $3,
|
||||
description = $4,
|
||||
created_at = $5
|
||||
WHERE id = $1
|
||||
AND user_id = $2;
|
||||
`;
|
||||
const params = [req.params.id, req.user?.id, seconds_spent, description || null, formatted_start];
|
||||
const result = await db.query(q, params);
|
||||
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 task_work_log
|
||||
WHERE id = $1
|
||||
AND task_id = $2
|
||||
AND user_id = $3;`;
|
||||
const result = await db.query(q, [req.params.id, req.query.task, req.user?.id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
private static async getExportMetadata(id: string) {
|
||||
const q = `SELECT name, (SELECT name FROM projects WHERE id = tasks.project_id) AS project_name
|
||||
FROM tasks
|
||||
WHERE id = $1;`;
|
||||
const result = await db.query(q, [id]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
private static async getUserTimeZone(id: string) {
|
||||
if (id) {
|
||||
const q = `SELECT utc_offset
|
||||
FROM timezones
|
||||
WHERE id = (SELECT timezone_id FROM users WHERE id = $1);`;
|
||||
const result = await db.query(q, [id]);
|
||||
const [data] = result.rows;
|
||||
return data.utc_offset || null;
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async exportLog(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<void> {
|
||||
const results = await this.getTimeLogs(req.params.id, req.query.timeZone as string);
|
||||
const metadata = await this.getExportMetadata(req.params.id);
|
||||
const timezone = await this.getUserTimeZone(req.user?.id || "");
|
||||
|
||||
const exportDate = moment().format("MMM-DD-YYYY");
|
||||
const fileName = `${exportDate} - Task Timelog`;
|
||||
const title = metadata.name.replace(/[\*\?\:\/\\\[\]]/g, "-");
|
||||
|
||||
const workbook = new Excel.Workbook();
|
||||
const sheet = workbook.addWorksheet(title);
|
||||
|
||||
sheet.headerFooter = {
|
||||
firstHeader: title
|
||||
};
|
||||
|
||||
sheet.columns = [
|
||||
{header: "Reporter Name", key: "user_name", width: 25},
|
||||
{header: "Reporter Email", key: "user_email", width: 25},
|
||||
{header: "Start Time", key: "start_time", width: 25},
|
||||
{header: "End Time", key: "end_time", width: 25},
|
||||
{header: "Date", key: "created_at", width: 25},
|
||||
{header: "Work Description", key: "description", width: 25},
|
||||
{header: "Duration", key: "time_spent", width: 25},
|
||||
];
|
||||
|
||||
sheet.getCell("A1").value = metadata.project_name;
|
||||
sheet.mergeCells("A1:G1");
|
||||
sheet.getCell("A1").alignment = {horizontal: "center"};
|
||||
|
||||
sheet.getCell("A2").value = `${metadata.name} (${exportDate})`;
|
||||
sheet.mergeCells("A2:G2");
|
||||
sheet.getCell("A2").alignment = {horizontal: "center"};
|
||||
|
||||
sheet.getRow(4).values = [
|
||||
"Reporter Name",
|
||||
"Reporter Email",
|
||||
"Start Time",
|
||||
"End Time",
|
||||
"Date",
|
||||
"Work Description",
|
||||
"Duration",
|
||||
];
|
||||
|
||||
const timeFormat = "MMM DD, YYYY h:mm:ss a";
|
||||
let totalLogged = 0;
|
||||
|
||||
for (const item of results) {
|
||||
totalLogged += parseFloat((item.time_spent || 0).toString());
|
||||
const data = {
|
||||
user_name: item.user_name,
|
||||
user_email: item.user_email,
|
||||
start_time: moment(item.start_time).add(timezone.hours || 0, "hours").add(timezone.minutes || 0, "minutes").format(timeFormat),
|
||||
end_time: moment(item.end_time).add(timezone.hours || 0, "hours").add(timezone.minutes || 0, "minutes").format(timeFormat),
|
||||
created_at: moment(item.created_at).add(timezone.hours || 0, "hours").add(timezone.minutes || 0, "minutes").format(timeFormat),
|
||||
description: item.description || "-",
|
||||
time_spent: formatDuration(moment.duration(item.time_spent, "seconds")),
|
||||
};
|
||||
sheet.addRow(data);
|
||||
}
|
||||
|
||||
sheet.getCell("A1").style.fill = {
|
||||
type: "pattern",
|
||||
pattern: "solid",
|
||||
fgColor: {argb: "D9D9D9"}
|
||||
};
|
||||
sheet.getCell("A1").font = {
|
||||
size: 16
|
||||
};
|
||||
|
||||
sheet.getCell("A2").style.fill = {
|
||||
type: "pattern",
|
||||
pattern: "solid",
|
||||
fgColor: {argb: "F2F2F2"}
|
||||
};
|
||||
sheet.getCell("A2").font = {
|
||||
size: 12
|
||||
};
|
||||
|
||||
sheet.getRow(4).font = {
|
||||
bold: true
|
||||
};
|
||||
|
||||
sheet.addRow({
|
||||
user_name: "",
|
||||
user_email: "",
|
||||
start_time: "Total",
|
||||
end_time: "",
|
||||
description: "",
|
||||
created_at: "",
|
||||
time_spent: formatDuration(moment.duration(totalLogged, "seconds")),
|
||||
});
|
||||
|
||||
sheet.mergeCells(`A${sheet.rowCount}:F${sheet.rowCount}`);
|
||||
|
||||
sheet.getCell(`A${sheet.rowCount}`).value = "Total";
|
||||
sheet.getCell(`A${sheet.rowCount}`).alignment = {
|
||||
horizontal: "right"
|
||||
};
|
||||
|
||||
res.setHeader("Content-Type", "application/vnd.openxmlformats");
|
||||
res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
|
||||
|
||||
await workbook.xlsx.write(res)
|
||||
.then(() => {
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
91
worklenz-backend/src/controllers/tasks-controller-base.ts
Normal file
91
worklenz-backend/src/controllers/tasks-controller-base.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import {getColor} from "../shared/utils";
|
||||
import {PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA} from "../shared/constants";
|
||||
import moment from "moment/moment";
|
||||
|
||||
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 TasksControllerBase extends WorklenzControllerBase {
|
||||
protected static calculateTaskCompleteRatio(totalCompleted: number, totalTasks: number) {
|
||||
if (totalCompleted === 0 && totalTasks === 0) return 0;
|
||||
const ratio = ((totalCompleted / totalTasks) * 100);
|
||||
return ratio == Infinity ? 100 : ratio.toFixed();
|
||||
}
|
||||
|
||||
public static updateTaskViewModel(task: any) {
|
||||
task.progress = ~~(task.total_minutes_spent / task.total_minutes * 100);
|
||||
task.overdue = task.total_minutes < task.total_minutes_spent;
|
||||
|
||||
task.time_spent = {hours: ~~(task.total_minutes_spent / 60), minutes: task.total_minutes_spent % 60};
|
||||
|
||||
task.comments_count = Number(task.comments_count) ? +task.comments_count : 0;
|
||||
task.attachments_count = Number(task.attachments_count) ? +task.attachments_count : 0;
|
||||
|
||||
if (typeof task.sub_tasks_count === "undefined") task.sub_tasks_count = "0";
|
||||
|
||||
task.is_sub_task = !!task.parent_task_id;
|
||||
|
||||
task.time_spent_string = `${task.time_spent.hours}h ${(task.time_spent.minutes)}m`;
|
||||
task.total_time_string = `${~~(task.total_minutes / 60)}h ${(task.total_minutes % 60)}m`;
|
||||
|
||||
task.name_color = getColor(task.name);
|
||||
task.priority_color = PriorityColorCodes[task.priority_value] || PriorityColorCodes["0"];
|
||||
task.show_sub_tasks = false;
|
||||
|
||||
if (task.phase_id) {
|
||||
task.phase_color = task.phase_color_code
|
||||
? task.phase_color_code : getColor(task.phase_name) + TASK_PRIORITY_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
if (Array.isArray(task.assignees)) {
|
||||
for (const assignee of task.assignees) {
|
||||
assignee.color_code = getColor(assignee.name);
|
||||
}
|
||||
}
|
||||
|
||||
task.names = TasksControllerBase.createTagList(task.assignees);
|
||||
|
||||
task.all_labels = task.labels;
|
||||
task.labels = TasksControllerBase.createTagList(task.labels, 2);
|
||||
|
||||
task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
|
||||
task.priority_color = task.priority_color + TASK_PRIORITY_COLOR_ALPHA;
|
||||
|
||||
if (task.timer_start_time)
|
||||
task.timer_start_time = moment(task.timer_start_time).valueOf();
|
||||
|
||||
const totalCompleted = +task.completed_sub_tasks + +task.parent_task_completed;
|
||||
const totalTasks = +task.sub_tasks_count + 1; // +1 for parent
|
||||
task.complete_ratio = TasksControllerBase.calculateTaskCompleteRatio(totalCompleted, totalTasks);
|
||||
task.completed_count = totalCompleted;
|
||||
task.total_tasks_count = totalTasks;
|
||||
|
||||
task.width = 35;
|
||||
|
||||
if (task.chart_start) {
|
||||
const fToday = moment().format("YYYY-MM-DD");
|
||||
task.offset_from = (moment(fToday).diff(task.chart_start, "days")) * 35;
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
}
|
||||
482
worklenz-backend/src/controllers/tasks-controller-v2.ts
Normal file
482
worklenz-backend/src/controllers/tasks-controller-v2.ts
Normal file
@@ -0,0 +1,482 @@
|
||||
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 TasksControllerBase, {GroupBy, ITaskGroup} from "./tasks-controller-base";
|
||||
|
||||
export class TaskListGroup implements ITaskGroup {
|
||||
name: string;
|
||||
category_id: string | null;
|
||||
color_code: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
todo_progress: number;
|
||||
doing_progress: number;
|
||||
done_progress: number;
|
||||
tasks: any[];
|
||||
|
||||
constructor(group: any) {
|
||||
this.name = group.name;
|
||||
this.category_id = group.category_id || null;
|
||||
this.start_date = group.start_date || null;
|
||||
this.end_date = group.end_date || null;
|
||||
this.color_code = group.color_code + TASK_STATUS_COLOR_ALPHA;
|
||||
this.todo_progress = 0;
|
||||
this.doing_progress = 0;
|
||||
this.done_progress = 0;
|
||||
this.tasks = [];
|
||||
}
|
||||
}
|
||||
|
||||
export default class TasksControllerV2 extends TasksControllerBase {
|
||||
private static isCountsOnly(query: ParsedQs) {
|
||||
return query.count === "true";
|
||||
}
|
||||
|
||||
public static isTasksOnlyReq(query: ParsedQs) {
|
||||
return TasksControllerV2.isCountsOnly(query) || query.parent_task;
|
||||
}
|
||||
|
||||
private static flatString(text: string) {
|
||||
return (text || "").split(" ").map(s => `'${s}'`).join(",");
|
||||
}
|
||||
|
||||
private static getFilterByStatusWhereClosure(text: string) {
|
||||
return text ? `status_id IN (${this.flatString(text)})` : "";
|
||||
}
|
||||
|
||||
private static getFilterByPriorityWhereClosure(text: string) {
|
||||
return text ? `priority_id IN (${this.flatString(text)})` : "";
|
||||
}
|
||||
|
||||
private static getFilterByLabelsWhereClosure(text: string) {
|
||||
return text
|
||||
? `id IN (SELECT task_id FROM task_labels WHERE label_id IN (${this.flatString(text)}))`
|
||||
: "";
|
||||
}
|
||||
|
||||
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 getFilterByProjectsWhereClosure(text: string) {
|
||||
return text ? `project_id IN (${this.flatString(text)})` : "";
|
||||
}
|
||||
|
||||
private static getFilterByAssignee(filterBy: string) {
|
||||
return filterBy === "member"
|
||||
? `id IN (SELECT task_id FROM tasks_assignees WHERE team_member_id = $1)`
|
||||
: "project_id = $1";
|
||||
}
|
||||
|
||||
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} = TasksControllerV2.toPaginationOptions(options, searchField);
|
||||
|
||||
const isSubTasks = !!options.parent_task;
|
||||
|
||||
const sortFields = sortField.replace(/ascend/g, "ASC").replace(/descend/g, "DESC") || "sort_order";
|
||||
|
||||
// Filter tasks by statuses
|
||||
const statusesFilter = TasksControllerV2.getFilterByStatusWhereClosure(options.statuses as string);
|
||||
// Filter tasks by labels
|
||||
const labelsFilter = TasksControllerV2.getFilterByLabelsWhereClosure(options.labels as string);
|
||||
// Filter tasks by its members
|
||||
const membersFilter = TasksControllerV2.getFilterByMembersWhereClosure(options.members as string);
|
||||
// Filter tasks by projects
|
||||
const projectsFilter = TasksControllerV2.getFilterByProjectsWhereClosure(options.projects as string);
|
||||
// Filter tasks by priorities
|
||||
const priorityFilter = TasksControllerV2.getFilterByPriorityWhereClosure(options.priorities as string);
|
||||
// Filter tasks by a single assignee
|
||||
const filterByAssignee = TasksControllerV2.getFilterByAssignee(options.filterBy as string);
|
||||
// Returns statuses of each task as a json array if filterBy === "member"
|
||||
const statusesQuery = TasksControllerV2.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),
|
||||
(isSubTasks ? "$1 = $1" : filterByAssignee), // ignored filter by member in peoples page for sub-tasks
|
||||
statusesFilter,
|
||||
priorityFilter,
|
||||
labelsFilter,
|
||||
membersFilter,
|
||||
projectsFilter
|
||||
].filter(i => !!i).join(" AND ");
|
||||
|
||||
return `
|
||||
SELECT id,
|
||||
name,
|
||||
CONCAT((SELECT key FROM projects WHERE id = t.project_id), '-', task_no) AS task_key,
|
||||
(SELECT name FROM projects WHERE id = t.project_id) AS project_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.description,
|
||||
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 project_phases
|
||||
WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_color_code,
|
||||
|
||||
(EXISTS(SELECT 1 FROM task_subscribers WHERE task_id = t.id)) AS has_subscribers,
|
||||
|
||||
(SELECT start_time
|
||||
FROM task_timers
|
||||
WHERE task_id = t.id
|
||||
AND user_id = '${userId}') AS timer_start_time,
|
||||
|
||||
(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,
|
||||
|
||||
(SELECT COUNT(*) FROM task_comments WHERE task_id = t.id) AS comments_count,
|
||||
(SELECT COUNT(*) FROM task_attachments WHERE task_id = t.id) AS attachments_count,
|
||||
(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 COALESCE(JSON_AGG(r), '[]'::JSON)
|
||||
FROM (SELECT task_labels.label_id AS id,
|
||||
(SELECT name FROM team_labels WHERE id = task_labels.label_id),
|
||||
(SELECT color_code FROM team_labels WHERE id = task_labels.label_id)
|
||||
FROM task_labels
|
||||
WHERE task_id = t.id) r) AS labels,
|
||||
|
||||
(SELECT name FROM users WHERE id = t.reporter_id) AS reporter,
|
||||
(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,
|
||||
(SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id) AS total_minutes_spent,
|
||||
created_at,
|
||||
updated_at,
|
||||
completed_at,
|
||||
start_date,
|
||||
END_DATE ${statusesQuery}
|
||||
FROM tasks t
|
||||
WHERE ${filters} ${searchQuery}
|
||||
ORDER BY ${sortFields}
|
||||
`;
|
||||
}
|
||||
|
||||
public static async getGroups(groupBy: string, projectId: 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 = 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, sort_index
|
||||
FROM project_phases
|
||||
WHERE project_id = $1
|
||||
ORDER BY sort_index DESC;
|
||||
`;
|
||||
params = [projectId];
|
||||
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 = TasksControllerV2.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 TaskListGroup(group);
|
||||
return g;
|
||||
}, {});
|
||||
|
||||
this.updateMapByGroup(tasks, groupBy, map);
|
||||
|
||||
const updatedGroups = Object.keys(map).map(key => {
|
||||
const group = map[key];
|
||||
|
||||
TasksControllerV2.updateTaskProgresses(group);
|
||||
|
||||
// if (groupBy === GroupBy.PHASE)
|
||||
// group.color_code = group.color_code + 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]: ITaskGroup }) {
|
||||
let index = 0;
|
||||
const unmapped = [];
|
||||
for (const task of tasks) {
|
||||
task.index = index++;
|
||||
TasksControllerV2.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: "#fbc84c69",
|
||||
tasks: unmapped
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static updateTaskProgresses(group: ITaskGroup) {
|
||||
const todoCount = group.tasks.filter(t => t.status_category?.is_todo).length;
|
||||
const doingCount = group.tasks.filter(t => t.status_category?.is_doing).length;
|
||||
const doneCount = group.tasks.filter(t => t.status_category?.is_done).length;
|
||||
|
||||
const total = group.tasks.length;
|
||||
|
||||
group.todo_progress = +this.calculateTaskCompleteRatio(todoCount, total);
|
||||
group.doing_progress = +this.calculateTaskCompleteRatio(doingCount, total);
|
||||
group.done_progress = +this.calculateTaskCompleteRatio(doneCount, total);
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTasksOnly(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const isSubTasks = !!req.query.parent_task;
|
||||
const q = TasksControllerV2.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) {
|
||||
TasksControllerV2.updateTaskViewModel(task);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async convertToTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
UPDATE tasks
|
||||
SET parent_task_id = NULL,
|
||||
sort_order = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = $2), 0)
|
||||
WHERE id = $1;
|
||||
`;
|
||||
await db.query(q, [req.body.id, req.body.project_id]);
|
||||
|
||||
const result = await db.query("SELECT get_single_task($1) AS task;", [req.body.id]);
|
||||
const [data] = result.rows;
|
||||
const model = TasksControllerV2.updateTaskViewModel(data.task);
|
||||
return res.status(200).send(new ServerResponse(true, model));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getNewKanbanTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {id} = req.params;
|
||||
const result = await db.query("SELECT get_single_task($1) AS task;", [id]);
|
||||
const [data] = result.rows;
|
||||
const task = TasksControllerV2.updateTaskViewModel(data.task);
|
||||
return res.status(200).send(new ServerResponse(true, task));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async convertToSubtask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
const groupType = req.body.group_by;
|
||||
let q = ``;
|
||||
|
||||
if (groupType == "status") {
|
||||
q = `
|
||||
UPDATE tasks
|
||||
SET parent_task_id = $3,
|
||||
sort_order = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = $2), 0),
|
||||
status_id = $4
|
||||
WHERE id = $1;
|
||||
`;
|
||||
} else if (groupType == "priority") {
|
||||
q = `
|
||||
UPDATE tasks
|
||||
SET parent_task_id = $3,
|
||||
sort_order = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = $2), 0),
|
||||
priority_id = $4
|
||||
WHERE id = $1;
|
||||
`;
|
||||
} else if (groupType === "phase") {
|
||||
await db.query(`
|
||||
UPDATE tasks
|
||||
SET parent_task_id = $3,
|
||||
sort_order = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = $2), 0)
|
||||
WHERE id = $1;
|
||||
`, [req.body.id, req.body.project_id, req.body.parent_task_id]);
|
||||
q = `SELECT handle_on_task_phase_change($1, $2);`;
|
||||
}
|
||||
|
||||
if (req.body.to_group_id === UNMAPPED)
|
||||
req.body.to_group_id = null;
|
||||
|
||||
const params = groupType === "phase"
|
||||
? [req.body.id, req.body.to_group_id]
|
||||
: [req.body.id, req.body.project_id, req.body.parent_task_id, req.body.to_group_id];
|
||||
await db.query(q, params);
|
||||
|
||||
const result = await db.query("SELECT get_single_task($1) AS task;", [req.body.id]);
|
||||
const [data] = result.rows;
|
||||
const model = TasksControllerV2.updateTaskViewModel(data.task);
|
||||
return res.status(200).send(new ServerResponse(true, model));
|
||||
}
|
||||
|
||||
public static async getTaskSubscribers(taskId: string) {
|
||||
const q = `
|
||||
SELECT u.name, u.avatar_url, ts.user_id, ts.team_member_id, ts.task_id
|
||||
FROM task_subscribers ts
|
||||
LEFT JOIN users u ON ts.user_id = u.id
|
||||
WHERE ts.task_id = $1;
|
||||
`;
|
||||
const result = await db.query(q, [taskId]);
|
||||
|
||||
for (const member of result.rows)
|
||||
member.color_code = getColor(member.name);
|
||||
|
||||
return this.createTagList(result.rows);
|
||||
}
|
||||
|
||||
public static async checkUserAssignedToTask(taskId: string, userId: string, teamId: string) {
|
||||
const q = `
|
||||
SELECT EXISTS(
|
||||
SELECT * FROM tasks_assignees WHERE task_id = $1 AND team_member_id = (SELECT team_member_id FROM team_member_info_view WHERE user_id = $2 AND team_id = $3)
|
||||
);
|
||||
`;
|
||||
const result = await db.query(q, [taskId, userId, teamId]);
|
||||
const [data] = result.rows;
|
||||
|
||||
return data.exists;
|
||||
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getSubscribers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const subscribers = await this.getTaskSubscribers(req.params.id);
|
||||
return res.status(200).send(new ServerResponse(true, subscribers));
|
||||
}
|
||||
}
|
||||
619
worklenz-backend/src/controllers/tasks-controller.ts
Normal file
619
worklenz-backend/src/controllers/tasks-controller.ts
Normal file
@@ -0,0 +1,619 @@
|
||||
import moment from "moment";
|
||||
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import { TASK_STATUS_COLOR_ALPHA } from "../shared/constants";
|
||||
import { getDates, getMinMaxOfTaskDates, getMonthRange, getWeekRange } from "../shared/tasks-controller-utils";
|
||||
import { getColor, getRandomColorCode, log_error, toMinutes } from "../shared/utils";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import { NotificationsService } from "../services/notifications/notifications.service";
|
||||
import { getTaskCompleteInfo } from "../socket.io/commands/on-quick-task";
|
||||
import { getAssignees, getTeamMembers } from "../socket.io/commands/on-quick-assign-or-remove";
|
||||
import TasksControllerV2 from "./tasks-controller-v2";
|
||||
import { IO } from "../shared/io";
|
||||
import { SocketEvents } from "../socket.io/events";
|
||||
import TasksControllerBase from "./tasks-controller-base";
|
||||
import { insertToActivityLogs, logStatusChange } from "../services/activity-logs/activity-logs.service";
|
||||
import { forEach } from "lodash";
|
||||
import { IActivityLog } from "../services/activity-logs/interfaces";
|
||||
|
||||
export default class TasksController extends TasksControllerBase {
|
||||
private static notifyProjectUpdates(socketId: string, projectId: string) {
|
||||
IO.getSocketById(socketId)
|
||||
?.to(projectId)
|
||||
.emit(SocketEvents.PROJECT_UPDATES_AVAILABLE.toString());
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT create_task($1) AS task;`;
|
||||
const result = await db.query(q, [JSON.stringify(req.body)]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const userId = req.user?.id as string;
|
||||
|
||||
for (const member of data?.task.assignees || []) {
|
||||
NotificationsService.createTaskUpdate(
|
||||
"ASSIGN",
|
||||
userId,
|
||||
data.task.id,
|
||||
member.user_id,
|
||||
member.team_id
|
||||
);
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.task));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getGanttTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT get_gantt_tasks($1) AS gantt_tasks;`;
|
||||
const result = await db.query(q, [req.user?.id ?? null]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data.gantt_tasks));
|
||||
}
|
||||
|
||||
private static sendAssignmentNotifications(task: any, userId: string) {
|
||||
const newMembers = task.new_assignees.filter((member1: any) => {
|
||||
return !task.old_assignees.some((member2: any) => {
|
||||
return member1.team_member_id === member2.team_member_id;
|
||||
});
|
||||
});
|
||||
const removedMembers = task.old_assignees.filter((member1: any) => {
|
||||
return !task.new_assignees.some((member2: any) => {
|
||||
return member1.team_member_id === member2.team_member_id;
|
||||
});
|
||||
});
|
||||
|
||||
for (const member of newMembers) {
|
||||
NotificationsService.createTaskUpdate(
|
||||
"ASSIGN",
|
||||
userId,
|
||||
task.id,
|
||||
member.user_id,
|
||||
member.team_id
|
||||
);
|
||||
}
|
||||
|
||||
for (const member of removedMembers) {
|
||||
NotificationsService.createTaskUpdate(
|
||||
"UNASSIGN",
|
||||
userId,
|
||||
task.id,
|
||||
member.user_id,
|
||||
member.team_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async notifyStatusChange(userId: string, taskId: string, statusId: string) {
|
||||
try {
|
||||
const q2 = "SELECT handle_on_task_status_change($1, $2, $3) AS res;";
|
||||
const results1 = await db.query(q2, [userId, taskId, statusId]);
|
||||
const [d] = results1.rows;
|
||||
const changeResponse = d.res;
|
||||
|
||||
// notify to all task members of the change
|
||||
for (const member of changeResponse.members || []) {
|
||||
if (member.user_id === userId) continue;
|
||||
NotificationsService.createNotification({
|
||||
userId: member.user_id,
|
||||
teamId: member.team_id,
|
||||
socketId: member.socket_id,
|
||||
message: changeResponse.message,
|
||||
taskId,
|
||||
projectId: changeResponse.project_id
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
}
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const userId = req.user?.id as string;
|
||||
|
||||
await this.notifyStatusChange(userId, req.body.id, req.body.status_id);
|
||||
|
||||
const q = `SELECT update_task($1) AS task;`;
|
||||
const result = await db.query(q, [JSON.stringify(req.body)]);
|
||||
const [data] = result.rows;
|
||||
const task = data.task || null;
|
||||
|
||||
if (task) {
|
||||
this.sendAssignmentNotifications(task, userId);
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateDuration(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
const { start, end } = req.body;
|
||||
|
||||
const q = `
|
||||
UPDATE tasks
|
||||
SET start_date = ($1)::TIMESTAMP,
|
||||
end_date = ($2)::TIMESTAMP
|
||||
WHERE id = ($3)::UUID
|
||||
RETURNING id;
|
||||
`;
|
||||
const result = await db.query(q, [start, end, id]);
|
||||
const [data] = result.rows;
|
||||
if (data?.id)
|
||||
return res.status(200).send(new ServerResponse(true, {}));
|
||||
return res.status(200).send(new ServerResponse(false, {}, "Task update failed!"));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { status_id, task_id } = req.params;
|
||||
const { project_id, from_index, to_index } = req.body;
|
||||
|
||||
const q = `SELECT update_task_status($1, $2, $3, $4, $5) AS status;`;
|
||||
const result = await db.query(q, [task_id, project_id, status_id, from_index, to_index]);
|
||||
const [data] = result.rows;
|
||||
if (data?.status) return res.status(200).send(new ServerResponse(true, {}));
|
||||
|
||||
return res.status(200).send(new ServerResponse(false, {}, "Task update failed!"));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
const q = `SELECT get_project_gantt_tasks($1) AS gantt_tasks;`;
|
||||
const result = await db.query(q, [id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data?.gantt_tasks));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTasksBetweenRange(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { project_id, start_date, end_date } = req.query;
|
||||
const q = `
|
||||
SELECT pm.id,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]' ::JSON)
|
||||
FROM (SELECT t.id,
|
||||
t.name,
|
||||
t.start_date,
|
||||
t.project_id,
|
||||
t.priority_id,
|
||||
t.done,
|
||||
t.end_date,
|
||||
(SELECT color_code
|
||||
FROM projects
|
||||
WHERE projects.id = t.project_id) AS color_code,
|
||||
(SELECT name FROM task_statuses WHERE id = t.status_id) AS status
|
||||
FROM tasks_assignees ta,
|
||||
tasks t
|
||||
WHERE t.archived IS FALSE
|
||||
AND ta.project_member_id = pm.id
|
||||
AND t.id = ta.task_id
|
||||
AND start_date IS NOT NULL
|
||||
AND end_date IS NOT NULL
|
||||
ORDER BY start_date) rec) AS tasks
|
||||
FROM project_members pm
|
||||
WHERE project_id = $1;
|
||||
`;
|
||||
const result = await db.query(q, [project_id]);
|
||||
const obj: any = {};
|
||||
|
||||
const minMaxDates: { min_date: string, max_date: string } = await getMinMaxOfTaskDates(project_id as string);
|
||||
|
||||
const dates = await getDates(minMaxDates.min_date || start_date as string, minMaxDates.max_date || end_date as string);
|
||||
const months = await getWeekRange(dates);
|
||||
|
||||
for (const element of result.rows) {
|
||||
obj[element.id] = element.tasks;
|
||||
for (const task of element.tasks) {
|
||||
const min: number = dates.findIndex((date) => moment(task.start_date).isSame(date.date, "days"));
|
||||
const max: number = dates.findIndex((date) => moment(task.end_date).isSame(date.date, "days"));
|
||||
task.min = min + 1;
|
||||
task.max = max > 0 ? max + 2 : max;
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, { tasks: [obj], dates, months }));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getGanttTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT id,
|
||||
name,
|
||||
start_date,
|
||||
project_id,
|
||||
priority_id,
|
||||
done,
|
||||
end_date,
|
||||
(SELECT color_code
|
||||
FROM projects
|
||||
WHERE projects.id = project_id) AS color_code,
|
||||
(SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status,
|
||||
parent_task_id,
|
||||
parent_task_id IS NOT NULL AS is_sub_task,
|
||||
(SELECT name FROM tasks WHERE id = tasks.parent_task_id) AS parent_task_name,
|
||||
(SELECT COUNT('*')::INT FROM tasks WHERE parent_task_id = tasks.id) AS sub_tasks_count
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = $1
|
||||
AND parent_task_id IS NULL
|
||||
ORDER BY start_date;
|
||||
`;
|
||||
const result = await db.query(q, [req.query.project_id]);
|
||||
|
||||
const minMaxDates: {
|
||||
min_date: string,
|
||||
max_date: string
|
||||
} = await getMinMaxOfTaskDates(req.query.project_id as string);
|
||||
|
||||
if (!minMaxDates.max_date && !minMaxDates.min_date) {
|
||||
minMaxDates.min_date = moment().format();
|
||||
minMaxDates.max_date = moment().add(45, "days").format();
|
||||
}
|
||||
|
||||
const dates = await getDates(minMaxDates.min_date, minMaxDates.max_date);
|
||||
const weeks = await getWeekRange(dates);
|
||||
const months = await getMonthRange(dates);
|
||||
|
||||
for (const task of result.rows) {
|
||||
const min: number = dates.findIndex((date) => moment(task.start_date).isSame(date.date, "days"));
|
||||
const max: number = dates.findIndex((date) => moment(task.end_date).isSame(date.date, "days"));
|
||||
task.show_sub_tasks = false;
|
||||
task.sub_tasks = [];
|
||||
task.min = min + 1;
|
||||
task.max = max > 0 ? max + 2 : max;
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, { tasks: result.rows, dates, weeks, months }));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjectTasksByTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT get_resource_gantt_tasks($1) AS gantt_tasks;`;
|
||||
const result = await db.query(q, [req.user?.id ?? null]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data?.gantt_tasks));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getSelectedTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT get_selected_tasks($1) AS tasks`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data?.tasks));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getUnselectedTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT get_unselected_tasks($1) AS tasks`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data?.tasks));
|
||||
}
|
||||
|
||||
/** Should migrate getProjectTasksByStatus to this */
|
||||
@HandleExceptions()
|
||||
public static async getProjectTasksByStatusV2(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
// Get all statuses
|
||||
const q1 = `
|
||||
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 = $1
|
||||
AND team_id = $2
|
||||
ORDER BY task_statuses.sort_order;
|
||||
`;
|
||||
const result1 = await db.query(q1, [req.query.project, req.user?.team_id]);
|
||||
const statuses = result1.rows;
|
||||
|
||||
const dataset = [];
|
||||
|
||||
// Query tasks of statuses
|
||||
for (const status of statuses) {
|
||||
const q2 = `SELECT get_tasks_by_status($1, $2) AS tasks`;
|
||||
const result2 = await db.query(q2, [req.params.id, status]);
|
||||
const [data] = result2.rows;
|
||||
|
||||
for (const task of data.tasks) {
|
||||
task.name_color = getColor(task.name);
|
||||
task.names = this.createTagList(task.assignees);
|
||||
task.names.map((a: any) => a.color_code = getColor(a.name));
|
||||
}
|
||||
dataset.push(data);
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, dataset));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjectTasksByStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT get_tasks_by_status($1,$2) AS tasks`;
|
||||
const result = await db.query(q, [req.params.id, req.query.status]);
|
||||
const [data] = result.rows;
|
||||
|
||||
for (const task of data.tasks) {
|
||||
task.name_color = getColor(task.name);
|
||||
task.names = this.createTagList(task.assignees);
|
||||
task.all_labels = task.labels;
|
||||
task.labels = this.createTagList(task.labels, 3);
|
||||
task.names.map((a: any) => a.color_code = getColor(a.name));
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data?.tasks));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `DELETE
|
||||
FROM tasks
|
||||
WHERE id = $1;`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT get_task_form_view_model($1, $2, $3, $4) AS view_model;`;
|
||||
const result = await db.query(q, [req.user?.id ?? null, req.user?.team_id ?? null, req.query.task_id ?? null, (req.query.project_id as string) || null]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const default_model = {
|
||||
task: {},
|
||||
priorities: [],
|
||||
projects: [],
|
||||
statuses: [],
|
||||
team_members: [],
|
||||
};
|
||||
|
||||
const task = data.view_model.task || null;
|
||||
|
||||
if (!task)
|
||||
return res.status(200).send(new ServerResponse(true, default_model));
|
||||
|
||||
if (data.view_model && task) {
|
||||
task.assignees.map((a: any) => {
|
||||
a.color_code = getColor(a.name);
|
||||
return a;
|
||||
});
|
||||
|
||||
task.names = WorklenzControllerBase.createTagList(task.assignees);
|
||||
|
||||
const totalMinutes = task.total_minutes;
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
|
||||
task.total_hours = hours;
|
||||
task.total_minutes = minutes;
|
||||
task.assignees = (task.assignees || []).map((i: any) => i.team_member_id);
|
||||
|
||||
task.timer_start_time = moment(task.timer_start_time).valueOf();
|
||||
|
||||
task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
|
||||
}
|
||||
|
||||
for (const member of (data.view_model?.team_members || [])) {
|
||||
member.color_code = getColor(member.name);
|
||||
}
|
||||
|
||||
const t = await getTaskCompleteInfo(task);
|
||||
const info = await TasksControllerV2.getTaskCompleteRatio(t.parent_task_id || t.id);
|
||||
|
||||
if (info) {
|
||||
t.complete_ratio = info.ratio;
|
||||
t.completed_count = info.total_completed;
|
||||
t.total_tasks_count = info.total_tasks;
|
||||
}
|
||||
|
||||
data.view_model.task = t;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.view_model || default_model));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async createQuickTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT create_quick_task($1) AS task_id;`;
|
||||
req.body.reporter_id = req.user?.id ?? null;
|
||||
req.body.team_id = req.user?.team_id ?? null;
|
||||
req.body.total_minutes = toMinutes(req.body.total_hours, req.body.total_minutes);
|
||||
const result = await db.query(q, [JSON.stringify(req.body)]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async createHomeTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT create_home_task($1);`;
|
||||
let endDate = req.body.end_date;
|
||||
switch (endDate) {
|
||||
case "Today":
|
||||
endDate = moment().format();
|
||||
break;
|
||||
case "Tomorrow":
|
||||
endDate = moment().add(1, "days").format();
|
||||
break;
|
||||
case "Next Week":
|
||||
endDate = moment().add(1, "weeks").endOf("isoWeek").format();
|
||||
break;
|
||||
case "Next Month":
|
||||
endDate = moment().add(1, "months").endOf("month").format();
|
||||
break;
|
||||
case "No Due Date":
|
||||
endDate = null;
|
||||
break;
|
||||
default:
|
||||
endDate = null;
|
||||
}
|
||||
req.body.end_date = endDate;
|
||||
req.body.reporter_id = req.user?.id ?? null;
|
||||
req.body.team_id = req.user?.team_id ?? null;
|
||||
const result = await db.query(q, [JSON.stringify(req.body)]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data.create_home_task.task));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async bulkChangeStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT bulk_change_tasks_status($1, $2) AS task;`;
|
||||
const result = await db.query(q, [JSON.stringify(req.body), req.user?.id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async bulkChangePriority(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT bulk_change_tasks_priority($1, $2) AS task;`;
|
||||
const result = await db.query(q, [JSON.stringify(req.body), req.user?.id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async bulkChangePhase(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT bulk_change_tasks_phase($1, $2) AS task;`;
|
||||
const result = await db.query(q, [JSON.stringify(req.body), req.user?.id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string);
|
||||
|
||||
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_tasks($1) AS task;`;
|
||||
await db.query(q, [JSON.stringify(req.body)]);
|
||||
TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string);
|
||||
return res.status(200).send(new ServerResponse(true, result));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async bulkArchive(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT bulk_archive_tasks($1) AS task;`;
|
||||
req.body.type = req.query.type;
|
||||
await db.query(q, [JSON.stringify(req.body)]);
|
||||
const tasks = req.body.tasks.map((t: any) => t.id);
|
||||
TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string);
|
||||
return res.status(200).send(new ServerResponse(true, tasks));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async bulkAssignMe(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
req.body.team_id = req.user?.team_id;
|
||||
req.body.user_id = req.user?.id;
|
||||
|
||||
const [task] = req.body.tasks || [];
|
||||
|
||||
const q = `SELECT bulk_assign_to_me($1) AS task;`;
|
||||
await db.query(q, [JSON.stringify(req.body)]);
|
||||
|
||||
const assignees = await getAssignees(task.id);
|
||||
const members = await getTeamMembers(req.body.team_id);
|
||||
// for inline display
|
||||
const names = WorklenzControllerBase.createTagList(assignees);
|
||||
|
||||
const data = { id: task.id, members, assignees, names };
|
||||
|
||||
const activityLog: IActivityLog = {
|
||||
task_id: task.id,
|
||||
attribute_type: "assignee",
|
||||
user_id: req.user?.id,
|
||||
log_type: "assign",
|
||||
old_value: null,
|
||||
new_value: req.user?.id,
|
||||
next_string: req.user?.name
|
||||
};
|
||||
|
||||
insertToActivityLogs(activityLog);
|
||||
|
||||
TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string);
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async bulkAssignLabel(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
|
||||
if (req.body.text) {
|
||||
const q0 = `SELECT bulk_assign_or_create_label($1) AS label;`;
|
||||
|
||||
req.body.team_id = req.user?.team_id;
|
||||
req.body.color = getRandomColorCode();
|
||||
|
||||
await db.query(q0, [JSON.stringify(req.body)]);
|
||||
} else {
|
||||
const q = `SELECT bulk_assign_label($1, $2) AS task;`;
|
||||
await db.query(q, [JSON.stringify(req.body), req.user?.id as string]);
|
||||
}
|
||||
|
||||
TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string);
|
||||
return res.status(200).send(new ServerResponse(true, null));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async bulkAssignMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { tasks, members, project_id } = req.body;
|
||||
try {
|
||||
for (const task of tasks) {
|
||||
for (const member of members) {
|
||||
await TasksController.createTaskBulkAssignees(member.id, project_id, task.id, req.user?.id as string);
|
||||
}
|
||||
}
|
||||
TasksController.notifyProjectUpdates(req.user?.socket_id as string, project_id as string);
|
||||
return res.status(200).send(new ServerResponse(true, null));
|
||||
} catch (error) {
|
||||
return res.status(500).send(new ServerResponse(false, "An error occurred"));
|
||||
}
|
||||
}
|
||||
|
||||
public static async createTaskAssignee(memberId: string, projectId: string, taskId: string, userId: string) {
|
||||
const q = `SELECT create_task_assignee($1,$2,$3,$4)`;
|
||||
const result = await db.query(q, [memberId, projectId, taskId, userId]);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
public static async createTaskBulkAssignees(memberId: string, projectId: string, taskId: string, userId: string) {
|
||||
const q = `SELECT create_bulk_task_assignees($1,$2,$3,$4)`;
|
||||
const result = await db.query(q, [memberId, projectId, taskId, userId]);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjectTaskAssignees(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT project_members.team_member_id AS id,
|
||||
tmiv.name,
|
||||
tmiv.email,
|
||||
tmiv.avatar_url
|
||||
FROM project_members
|
||||
LEFT JOIN team_member_info_view tmiv ON project_members.team_member_id = tmiv.team_member_id
|
||||
WHERE project_id = $1
|
||||
AND EXISTS(SELECT 1 FROM tasks_assignees WHERE project_member_id = project_members.id);
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
|
||||
for (const member of result.rows) {
|
||||
member.color_code = getColor(member.name);
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class TasksCustomColumnsController extends WorklenzControllerBase {
|
||||
|
||||
// Columns
|
||||
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
return res.status(200).send(new ServerResponse(true, []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
return res.status(200).send(new ServerResponse(true, []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
return res.status(200).send(new ServerResponse(true, []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async delete(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
return res.status(200).send(new ServerResponse(true, []));
|
||||
}
|
||||
|
||||
// Options
|
||||
|
||||
@HandleExceptions()
|
||||
public static async createOption(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
return res.status(200).send(new ServerResponse(true, []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getOption(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
return res.status(200).send(new ServerResponse(true, []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateOption(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
return res.status(200).send(new ServerResponse(true, []));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteOption(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
return res.status(200).send(new ServerResponse(true, []));
|
||||
}
|
||||
}
|
||||
905
worklenz-backend/src/controllers/team-members-controller.ts
Normal file
905
worklenz-backend/src/controllers/team-members-controller.ts
Normal file
@@ -0,0 +1,905 @@
|
||||
import moment from "moment";
|
||||
import Excel from "exceljs";
|
||||
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import { IPassportSession } from "../interfaces/passport-session";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import { sendInvitationEmail } from "../shared/email-templates";
|
||||
import { IO } from "../shared/io";
|
||||
import { SocketEvents } from "../socket.io/events";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import { formatDuration, getColor } from "../shared/utils";
|
||||
import { TEAM_MEMBER_TREE_MAP_COLOR_ALPHA } from "../shared/constants";
|
||||
import { NotificationsService } from "../services/notifications/notifications.service";
|
||||
|
||||
export default class TeamMembersController extends WorklenzControllerBase {
|
||||
|
||||
public static async checkIfUserAlreadyExists(owner_id: string, email: string) {
|
||||
if (!owner_id) throw new Error("Owner not found.");
|
||||
|
||||
const q = `SELECT EXISTS(SELECT tmi.team_member_id
|
||||
FROM team_member_info_view AS tmi
|
||||
JOIN teams AS t ON tmi.team_id = t.id
|
||||
WHERE tmi.email = $1::TEXT
|
||||
AND t.user_id = $2::UUID);`;
|
||||
const result = await db.query(q, [email, owner_id]);
|
||||
|
||||
const [data] = result.rows;
|
||||
return data.exists;
|
||||
}
|
||||
|
||||
public static async checkIfUserActiveInOtherTeams(owner_id: string, email: string) {
|
||||
if (!owner_id) throw new Error("Owner not found.");
|
||||
|
||||
const q = `SELECT EXISTS(SELECT tmi.team_member_id
|
||||
FROM team_member_info_view AS tmi
|
||||
JOIN teams AS t ON tmi.team_id = t.id
|
||||
JOIN team_members AS tm ON tmi.team_member_id = tm.id
|
||||
WHERE tmi.email = $1::TEXT
|
||||
AND t.user_id = $2::UUID AND tm.active = true);`;
|
||||
const result = await db.query(q, [email, owner_id]);
|
||||
|
||||
const [data] = result.rows;
|
||||
return data.exists;
|
||||
}
|
||||
|
||||
public static async createOrInviteMembers<T>(body: T, user: IPassportSession): Promise<Array<{
|
||||
name?: string;
|
||||
email?: string;
|
||||
is_new?: string;
|
||||
team_member_id?: string;
|
||||
team_member_user_id?: string;
|
||||
}>> {
|
||||
const q = `SELECT create_team_member($1) AS new_members;`;
|
||||
const result = await db.query(q, [JSON.stringify(body)]);
|
||||
|
||||
const [data] = result.rows;
|
||||
const newMembers = data?.new_members || [];
|
||||
|
||||
|
||||
const projectId = (body as any)?.project_id;
|
||||
|
||||
NotificationsService.sendTeamMembersInvitations(newMembers, user, projectId || "");
|
||||
|
||||
return newMembers;
|
||||
}
|
||||
|
||||
@HandleExceptions({
|
||||
raisedExceptions: {
|
||||
"ERROR_EMAIL_INVITATION_EXISTS": `Team member with email "{0}" already exists.`
|
||||
}
|
||||
})
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
req.body.team_id = req.user?.team_id || null;
|
||||
|
||||
if (!req.user?.team_id) {
|
||||
return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or invites new members based on the request body and user information.
|
||||
* Sends a response with the result.
|
||||
*/
|
||||
const newMembers = await this.createOrInviteMembers(req.body, req.user);
|
||||
return res.status(200).send(new ServerResponse(true, newMembers, `Your teammates will get an email that gives them access to your team.`).withTitle("Invitations sent"));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
req.query.field = ["is_owner", "active", "u.name", "u.email"];
|
||||
req.query.order = "descend";
|
||||
|
||||
const {
|
||||
searchQuery,
|
||||
sortField,
|
||||
sortOrder,
|
||||
size,
|
||||
offset
|
||||
} = this.toPaginationOptions(req.query, ["u.name", "u.email"], true);
|
||||
|
||||
const paginate = req.query.all === "false" ? `LIMIT ${size} OFFSET ${offset}` : "";
|
||||
|
||||
const q = `
|
||||
SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT team_members.id,
|
||||
(SELECT name
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = team_members.id),
|
||||
u.avatar_url,
|
||||
(u.socket_id IS NOT NULL) AS is_online,
|
||||
(SELECT COUNT(*)
|
||||
FROM project_members
|
||||
WHERE team_member_id = team_members.id) AS projects_count,
|
||||
(SELECT name FROM job_titles WHERE id = team_members.job_title_id) AS job_title,
|
||||
(SELECT name FROM roles WHERE id = team_members.role_id) AS role_name,
|
||||
EXISTS(SELECT id
|
||||
FROM roles
|
||||
WHERE id = team_members.role_id
|
||||
AND admin_role IS TRUE) AS is_admin,
|
||||
(CASE
|
||||
WHEN user_id = (SELECT user_id FROM teams WHERE id = $1) THEN TRUE
|
||||
ELSE FALSE END) AS is_owner,
|
||||
(SELECT email
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = team_members.id),
|
||||
EXISTS(SELECT email
|
||||
FROM email_invitations
|
||||
WHERE team_member_id = team_members.id
|
||||
AND email_invitations.team_id = team_members.team_id) AS pending_invitation,
|
||||
active
|
||||
FROM team_members
|
||||
LEFT JOIN users u ON team_members.user_id = u.id
|
||||
WHERE ${searchQuery} team_id = $1
|
||||
ORDER BY ${sortField} ${sortOrder} ${paginate}) t) AS data
|
||||
FROM team_members
|
||||
LEFT JOIN users u ON team_members.user_id = u.id
|
||||
WHERE ${searchQuery} team_id = $1
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.team_id || null]);
|
||||
const [members] = result.rows;
|
||||
|
||||
members.data?.map((a: any) => {
|
||||
a.color_code = getColor(a.name);
|
||||
return a;
|
||||
});
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, members || this.paginatedDatasetDefaultStruct));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getAllMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT get_team_members($1, $2) AS members;`;
|
||||
const result = await db.query(q, [req.user?.team_id || null, req.query.project || null]);
|
||||
|
||||
const [data] = result.rows;
|
||||
const members = data?.members || [];
|
||||
|
||||
for (const member of members) {
|
||||
member.color_code = getColor(member.name);
|
||||
member.usage = +member.usage;
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, members));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT id,
|
||||
created_at,
|
||||
updated_at,
|
||||
(SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = team_members.id),
|
||||
(SELECT avatar_url FROM users WHERE id = team_members.user_id),
|
||||
EXISTS(SELECT email
|
||||
FROM email_invitations
|
||||
WHERE team_member_id = team_members.id
|
||||
AND email_invitations.team_id = team_members.team_id) AS pending_invitation,
|
||||
(SELECT name FROM job_titles WHERE id = team_members.job_title_id) AS job_title,
|
||||
COALESCE(
|
||||
(SELECT email FROM users WHERE id = team_members.user_id),
|
||||
(SELECT email
|
||||
FROM email_invitations
|
||||
WHERE email_invitations.team_member_id = team_members.id
|
||||
AND email_invitations.team_id = team_members.team_id
|
||||
LIMIT 1)
|
||||
) AS email,
|
||||
EXISTS(SELECT id FROM roles WHERE id = team_members.role_id AND admin_role IS TRUE) AS is_admin
|
||||
FROM team_members
|
||||
WHERE id = $1
|
||||
AND team_id = $2;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTeamMembersByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT project_members.id,
|
||||
team_member_id,
|
||||
project_access_level_id,
|
||||
(SELECT name
|
||||
FROM project_access_levels
|
||||
WHERE id = project_access_level_id) AS project_access_level_name,
|
||||
(SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id),
|
||||
u.avatar_url,
|
||||
(SELECT team_member_info_view.email
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = tm.id)
|
||||
FROM project_members
|
||||
INNER JOIN team_members tm ON project_members.team_member_id = tm.id
|
||||
LEFT JOIN users u ON tm.user_id = u.id
|
||||
WHERE project_id = $1
|
||||
ORDER BY project_members.created_at DESC;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
req.body.id = req.params.id;
|
||||
req.body.team_id = req.user?.team_id || null;
|
||||
req.body.is_admin = !!req.body.is_admin;
|
||||
|
||||
const q = `SELECT update_team_member($1) AS team_member;`;
|
||||
const result = await db.query(q, [JSON.stringify(req.body)]);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async resend_invitation(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
req.body.team_id = req.user?.team_id || null;
|
||||
|
||||
const q = `SELECT resend_team_invitation($1) AS invitation;`;
|
||||
const result = await db.query(q, [JSON.stringify(req.body)]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (!data?.invitation || !data?.invitation.email)
|
||||
return res.status(200).send(new ServerResponse(false, null, "Resend failed! Please try again."));
|
||||
|
||||
const member = data.invitation;
|
||||
|
||||
sendInvitationEmail(
|
||||
!member.is_new,
|
||||
req.user as IPassportSession,
|
||||
!member.is_new ? member.name : member.team_member_id,
|
||||
member.email,
|
||||
member.team_member_user_id,
|
||||
member.name || member.email?.split("@")[0]
|
||||
);
|
||||
|
||||
if (member.team_member_id) {
|
||||
NotificationsService.sendInvitation(
|
||||
req.user?.id as string,
|
||||
req.user?.name as string,
|
||||
req.user?.team_name as string,
|
||||
req.user?.team_id as string,
|
||||
member.team_member_id
|
||||
);
|
||||
}
|
||||
|
||||
member.id = member.team_member_id;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, null, "Invitation resent"));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || !req.user?.team_id) return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
|
||||
|
||||
const q = `SELECT remove_team_member($1, $2, $3) AS member;`;
|
||||
const result = await db.query(q, [id, req.user?.id, req.user?.team_id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const message = `You have been removed from <b>${req.user?.team_name}</b> by <b>${req.user?.name}</b>`;
|
||||
|
||||
NotificationsService.sendNotification({
|
||||
receiver_socket_id: data.socket_id,
|
||||
message,
|
||||
team: data.team,
|
||||
team_id: id
|
||||
});
|
||||
|
||||
IO.emitByUserId(data.member.id, req.user?.id || null, SocketEvents.TEAM_MEMBER_REMOVED, {
|
||||
teamId: id,
|
||||
message
|
||||
});
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT (SELECT name FROM projects WHERE id = project_members.project_id) AS name,
|
||||
(SELECT COUNT(*) FROM tasks_assignees WHERE project_member_id = project_members.id) AS assigned_task_count,
|
||||
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
INNER JOIN tasks t ON tasks_assignees.task_id = t.id
|
||||
INNER JOIN task_statuses ts ON t.status_id = ts.id
|
||||
WHERE t.archived IS FALSE
|
||||
AND project_member_id = project_members.id
|
||||
AND ts.category_id IN
|
||||
(SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE)) AS done_task_count,
|
||||
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
INNER JOIN tasks t ON tasks_assignees.task_id = t.id
|
||||
INNER JOIN task_statuses ts ON t.status_id = ts.id
|
||||
WHERE t.archived IS FALSE
|
||||
AND project_member_id = project_members.id
|
||||
AND ts.category_id IN
|
||||
(SELECT id
|
||||
FROM sys_task_status_categories
|
||||
WHERE is_doing IS TRUE
|
||||
OR is_todo IS TRUE)) AS pending_task_count
|
||||
|
||||
FROM project_members
|
||||
WHERE team_member_id = $1;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
|
||||
for (const object of result.rows) {
|
||||
object.progress =
|
||||
object.assigned_task_count > 0
|
||||
? (
|
||||
(object.done_task_count / object.assigned_task_count) *
|
||||
100
|
||||
).toFixed(0)
|
||||
: 0;
|
||||
}
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getOverviewChart(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
INNER JOIN tasks t ON tasks_assignees.task_id = t.id
|
||||
INNER JOIN task_statuses ts ON t.status_id = ts.id
|
||||
WHERE t.archived IS FALSE AND project_member_id IN
|
||||
(SELECT id FROM project_members WHERE team_member_id = $1)
|
||||
AND ts.category_id IN (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE)) AS done_count,
|
||||
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
INNER JOIN tasks t ON tasks_assignees.task_id = t.id
|
||||
INNER JOIN task_statuses ts ON t.status_id = ts.id
|
||||
WHERE t.archived IS FALSE AND project_member_id IN
|
||||
(SELECT id FROM project_members WHERE team_member_id = $1)
|
||||
AND ts.category_id IN
|
||||
(SELECT id FROM sys_task_status_categories WHERE is_doing IS TRUE OR is_todo IS TRUE)) AS pending_count;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTeamMembersTreeMap(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { selected, team, archived } = req.query;
|
||||
|
||||
let q = "";
|
||||
|
||||
if (selected === "time") {
|
||||
q = `SELECT ROW_TO_JSON(rec) AS team_members
|
||||
FROM (SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT team_members.id,
|
||||
(SELECT COUNT(*)
|
||||
FROM project_members
|
||||
WHERE team_member_id = team_members.id
|
||||
AND CASE
|
||||
WHEN ($3 IS TRUE) THEN project_id IS NOT NULL
|
||||
ELSE project_id NOT IN (SELECT project_id
|
||||
FROM archived_projects
|
||||
WHERE archived_projects.project_id = project_members.project_id
|
||||
AND archived_projects.user_id = $2) END) AS projects_count,
|
||||
(SELECT SUM(time_spent)
|
||||
FROM task_work_log
|
||||
WHERE user_id = team_members.user_id
|
||||
AND task_id IN (SELECT id
|
||||
FROM tasks
|
||||
WHERE project_id IN (SELECT id
|
||||
FROM projects
|
||||
WHERE team_id = $1)
|
||||
AND CASE
|
||||
WHEN ($3 IS TRUE) THEN project_id IS NOT NULL
|
||||
ELSE project_id NOT IN (SELECT project_id
|
||||
FROM archived_projects
|
||||
WHERE archived_projects.project_id = tasks.project_id
|
||||
AND archived_projects.user_id = $2) END)) AS time_logged,
|
||||
(SELECT name
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = team_members.id),
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT project_id,
|
||||
(SELECT name
|
||||
FROM projects
|
||||
WHERE projects.id = project_members.project_id),
|
||||
(SELECT SUM(time_spent)
|
||||
FROM task_work_log
|
||||
WHERE task_work_log.task_id IN (SELECT id
|
||||
FROM tasks
|
||||
WHERE tasks.project_id = project_members.project_id)
|
||||
AND task_work_log.user_id IN (SELECT user_id
|
||||
FROM team_members
|
||||
WHERE team_member_id = team_members.id)
|
||||
AND task_id IN (SELECT id
|
||||
FROM tasks
|
||||
WHERE id = task_work_log.task_id
|
||||
AND CASE
|
||||
WHEN ($3 IS TRUE)
|
||||
THEN project_id IS NOT NULL
|
||||
ELSE project_id NOT IN (SELECT project_id
|
||||
FROM archived_projects
|
||||
WHERE archived_projects.project_id = tasks.project_id
|
||||
AND archived_projects.user_id = $2) END)) AS value
|
||||
FROM project_members
|
||||
WHERE team_member_id = team_members.id) t) AS projects
|
||||
FROM team_members
|
||||
LEFT JOIN users u ON team_members.user_id = u.id
|
||||
WHERE team_id = $1) t) AS data
|
||||
FROM team_members
|
||||
LEFT JOIN users u ON team_members.user_id = u.id
|
||||
WHERE team_id = $1) rec;`;
|
||||
}
|
||||
|
||||
if (selected === "tasks") {
|
||||
q = `SELECT ROW_TO_JSON(rec) AS team_members
|
||||
FROM (SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT team_members.id,
|
||||
(SELECT COUNT(*)
|
||||
FROM project_members
|
||||
WHERE team_member_id = team_members.id
|
||||
AND CASE
|
||||
WHEN ($3 IS FALSE) THEN project_id IS NOT NULL
|
||||
ELSE project_id NOT IN (SELECT project_id
|
||||
FROM archived_projects
|
||||
WHERE archived_projects.project_id = project_members.project_id
|
||||
AND archived_projects.user_id = $2) END) AS projects_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
WHERE team_member_id = team_members.id
|
||||
AND CASE
|
||||
WHEN ($3 IS FALSE) THEN task_id IN (SELECT id
|
||||
FROM tasks
|
||||
WHERE id = tasks_assignees.task_id
|
||||
AND project_id NOT IN
|
||||
(SELECT project_id
|
||||
FROM archived_projects
|
||||
WHERE archived_projects.project_id = tasks.project_id
|
||||
AND archived_projects.user_id = $2))
|
||||
ELSE task_id IS NOT NULL END) AS task_count,
|
||||
(SELECT name
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = team_members.id),
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT project_id,
|
||||
(SELECT name
|
||||
FROM projects
|
||||
WHERE projects.id = project_members.project_id),
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
WHERE project_member_id = project_members.id) AS value
|
||||
FROM project_members
|
||||
WHERE team_member_id = team_members.id
|
||||
AND CASE
|
||||
WHEN ($3 IS FALSE) THEN project_id IS NOT NULL
|
||||
ELSE project_id NOT IN (SELECT project_id
|
||||
FROM archived_projects
|
||||
WHERE archived_projects.project_id = project_members.project_id
|
||||
AND archived_projects.user_id = $2) END) t) AS projects
|
||||
FROM team_members
|
||||
LEFT JOIN users u ON team_members.user_id = u.id
|
||||
WHERE team_id = $1) t) AS DATA
|
||||
FROM team_members
|
||||
LEFT JOIN users u
|
||||
ON team_members.user_id = u.id
|
||||
WHERE team_id = $1) rec`;
|
||||
}
|
||||
|
||||
const result = await db.query(q, [team, req.user?.id, archived]);
|
||||
const [data] = result.rows;
|
||||
|
||||
const obj: any[] = [];
|
||||
|
||||
data.team_members.data.forEach((element: {
|
||||
id: string;
|
||||
name: string;
|
||||
projects_count: number;
|
||||
task_count: number;
|
||||
projects: any[];
|
||||
time_logged: number;
|
||||
}) => {
|
||||
obj.push({
|
||||
id: element.id,
|
||||
name: element.name,
|
||||
value: selected === "time" ? element.time_logged || 1 : element.task_count || 0,
|
||||
color: getColor(element.name) + TEAM_MEMBER_TREE_MAP_COLOR_ALPHA,
|
||||
label: selected === "time"
|
||||
? formatDuration(moment.duration(element.time_logged || "0", "seconds"))
|
||||
: `<br>${element.task_count} total tasks`,
|
||||
labelToolTip: selected === "time"
|
||||
? formatDuration(moment.duration(element.time_logged || "0", "seconds"))
|
||||
: `<b><br> - ${element.projects_count} projects <br> - ${element.task_count} total tasks</br>`
|
||||
});
|
||||
if (element.projects.length) {
|
||||
element.projects.forEach(item => {
|
||||
obj.push({
|
||||
id: item.project_id,
|
||||
name: item.name,
|
||||
parent: element.id,
|
||||
value: item.value || 1,
|
||||
label: selected === "time" ? formatDuration(moment.duration(item.value || "0", "seconds")) : `${item.value} tasks`
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
data.team_members.data = obj;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data.team_members));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getProjectsByTeamMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { project, status, startDate, endDate } = req.query;
|
||||
|
||||
let projectsString, statusString, dateFilterString1, dateFilterString2, dateFilterString3 = "";
|
||||
|
||||
if (project && typeof project === "string") {
|
||||
const projects = project.split(",").map(s => `'${s}'`).join(",");
|
||||
projectsString = `AND project_id IN (${projects})`;
|
||||
}
|
||||
|
||||
if (status && typeof status === "string") {
|
||||
const statuses = status.split(",").map(s => `'${s}'`).join(",");
|
||||
statusString = `AND status_id IN (${statuses})`;
|
||||
}
|
||||
|
||||
if (startDate && endDate) {
|
||||
dateFilterString1 = `AND twl2.created_at::DATE BETWEEN ${startDate}::DATE AND ${endDate}::DATE) AS total_logged_time`;
|
||||
dateFilterString2 = `LEFT JOIN tasks t ON p.id = t.project_id LEFT JOIN task_work_log twl ON t.id = twl.task_id`;
|
||||
dateFilterString3 = `AND twl.user_id = (SELECT user_id FROM team_members WHERE id = project_members.team_member_id)
|
||||
AND twl.created_at::DATE BETWEEN ${startDate}::DATE AND ${endDate}::DATE;`;
|
||||
}
|
||||
|
||||
const q = `
|
||||
(SELECT color_code,
|
||||
name,
|
||||
(SELECT count(*)
|
||||
FROM tasks_assignees
|
||||
WHERE project_members.team_member_id = tasks_assignees.team_member_id
|
||||
AND task_id IN (SELECT id FROM tasks WHERE tasks.project_id = projects.id)) AS task_count,
|
||||
(SELECT name FROM teams WHERE teams.id = projects.team_id) AS team,
|
||||
(SELECT sum(time_spent)
|
||||
FROM task_work_log
|
||||
WHERE task_id IN (SELECT id
|
||||
FROM tasks
|
||||
WHERE tasks.project_id = projects.id
|
||||
AND task_work_log.user_id =
|
||||
(SELECT user_id FROM team_members WHERE id = project_members.team_member_id)) ${dateFilterString1}) AS total_logged_time
|
||||
FROM project_members
|
||||
LEFT JOIN projects ON project_id = projects.id
|
||||
${dateFilterString2}
|
||||
WHERE team_member_id = $1 ${projectsString} ${statusString} ${dateFilterString3}
|
||||
ORDER BY name)`;
|
||||
const result = await db.query(q, [req.params.id]);
|
||||
|
||||
result.rows.forEach((element: { total_logged_time: string; }) => {
|
||||
element.total_logged_time = formatDuration(moment.duration(element.total_logged_time || "0", "seconds"));
|
||||
});
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTasksByMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT name,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
WHERE team_member_info_view.team_member_id = tasks_assignees.team_member_id) ::INT AS y
|
||||
FROM team_member_info_view
|
||||
WHERE team_id = $1
|
||||
ORDER BY name;`;
|
||||
const result = await db.query(q, [req.user?.team_id]);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
public static async getTeamMemberInsightData(team_id: string | undefined, start: any, end: any, project: any, status: any, searchQuery: string, sortField: string, sortOrder: string, size: any, offset: any, all: any) {
|
||||
let timeRangeTaskWorkLog = "";
|
||||
let projectsFilterString = "";
|
||||
let statusFilterString = "";
|
||||
|
||||
if (start && end) {
|
||||
timeRangeTaskWorkLog = `AND EXISTS(SELECT id FROM task_work_log
|
||||
WHERE created_at::DATE BETWEEN '${start}'::DATE AND '${end}'::DATE
|
||||
AND task_work_log.user_id = u.id)`;
|
||||
}
|
||||
|
||||
if (project && typeof project === "string") {
|
||||
const projects = project.split(",").map(s => `'${s}'`).join(",");
|
||||
projectsFilterString = `AND team_members.id IN (SELECT team_member_id FROM project_members WHERE project_id IN (${projects}))`;
|
||||
}
|
||||
|
||||
if (status && typeof status === "string") {
|
||||
const projects = status.split(",").map(s => `'${s}'`).join(",");
|
||||
statusFilterString = `AND team_members.id IN (SELECT team_member_id
|
||||
FROM project_members
|
||||
WHERE project_id IN (SELECT id
|
||||
FROM projects
|
||||
WHERE projects.team_id = '${team_id}'
|
||||
AND status_id IN (${projects})))`;
|
||||
}
|
||||
|
||||
const paginate = all === "false" ? `LIMIT ${size} OFFSET ${offset}` : "";
|
||||
|
||||
const q = `
|
||||
SELECT ROW_TO_JSON(rec) AS team_members
|
||||
FROM (SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT team_members.id,
|
||||
(SELECT name
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = team_members.id),
|
||||
u.avatar_url,
|
||||
(u.socket_id IS NOT NULL) AS is_online,
|
||||
(SELECT COUNT(*)
|
||||
FROM project_members
|
||||
WHERE team_member_id = team_members.id) AS projects_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_assignees
|
||||
WHERE team_member_id = team_members.id) AS task_count,
|
||||
(SELECT SUM(time_spent)
|
||||
FROM task_work_log
|
||||
WHERE task_work_log.user_id = tmiv.user_id
|
||||
AND task_id IN (SELECT id
|
||||
FROM tasks
|
||||
WHERE project_id IN (SELECT id
|
||||
FROM projects
|
||||
WHERE team_id = $1))) AS total_logged_time_seconds,
|
||||
(SELECT name FROM job_titles WHERE id = team_members.job_title_id) AS job_title,
|
||||
(SELECT name FROM roles WHERE id = team_members.role_id) AS role_name,
|
||||
EXISTS(SELECT id
|
||||
FROM roles
|
||||
WHERE id = team_members.role_id
|
||||
AND admin_role IS TRUE) AS is_admin,
|
||||
(CASE
|
||||
WHEN team_members.user_id = (SELECT user_id FROM teams WHERE id = $1) THEN TRUE
|
||||
ELSE FALSE END) AS is_owner,
|
||||
(SELECT email
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = team_members.id),
|
||||
EXISTS(SELECT email
|
||||
FROM email_invitations
|
||||
WHERE team_member_id = team_members.id
|
||||
AND email_invitations.team_id = team_members.team_id) AS pending_invitation,
|
||||
(SELECT (ARRAY(SELECT NAME
|
||||
FROM teams
|
||||
WHERE id IN (SELECT team_id
|
||||
FROM team_members
|
||||
WHERE team_members.user_id = tmiv.user_id)))) AS member_teams
|
||||
FROM team_members
|
||||
LEFT JOIN users u ON team_members.user_id = u.ID ${timeRangeTaskWorkLog}
|
||||
LEFT JOIN team_member_info_view tmiv ON team_members.id = tmiv.team_member_id
|
||||
WHERE team_members.team_id = $1 ${searchQuery} ${timeRangeTaskWorkLog} ${projectsFilterString} ${statusFilterString}
|
||||
ORDER BY ${sortField} ${sortOrder} ${paginate}) t) AS data
|
||||
FROM team_members
|
||||
LEFT JOIN users u ON team_members.user_id = u.ID ${timeRangeTaskWorkLog}
|
||||
LEFT JOIN team_member_info_view tmiv ON team_members.id = tmiv.team_member_id
|
||||
WHERE team_members.team_id = $1 ${searchQuery} ${timeRangeTaskWorkLog} ${projectsFilterString} ${statusFilterString}) rec;
|
||||
`;
|
||||
const result = await db.query(q, [team_id || null]);
|
||||
const [data] = result.rows;
|
||||
|
||||
return data.team_members;
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTeamMemberList(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {
|
||||
searchQuery,
|
||||
sortField,
|
||||
sortOrder,
|
||||
size,
|
||||
offset
|
||||
} = this.toPaginationOptions(req.query, ["tmiv.name", "tmiv.email", "u.name"]);
|
||||
const { start, end, project, status, teamId } = req.query;
|
||||
|
||||
const teamMembers = await this.getTeamMemberInsightData(teamId as string, start, end, project, status, searchQuery, sortField, sortOrder, size, offset, req.query.all);
|
||||
|
||||
teamMembers.data.map((a: any) => {
|
||||
a.color_code = getColor(a.name);
|
||||
a.total_logged_time = formatDuration(moment.duration(a.total_logged_time_seconds || "0", "seconds"));
|
||||
});
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, teamMembers || this.paginatedDatasetDefaultStruct));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTreeDataByMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { selected, id } = req.query;
|
||||
|
||||
let valueString = `(SELECT sum(time_spent)
|
||||
FROM task_work_log
|
||||
WHERE task_work_log.task_id IN (SELECT id
|
||||
FROM tasks
|
||||
WHERE tasks.project_id = project_members.project_id)
|
||||
AND task_work_log.user_id IN (SELECT user_id
|
||||
FROM team_members
|
||||
WHERE team_member_id = team_members.id))::INT AS value`;
|
||||
|
||||
if (selected === "tasks") {
|
||||
valueString = `(SELECT count(*) FROM tasks_assignees
|
||||
WHERE project_member_id = project_members.id)::INT AS value`;
|
||||
}
|
||||
|
||||
const q = `
|
||||
SELECT project_id,
|
||||
(SELECT name FROM projects WHERE projects.id = project_members.project_id),
|
||||
(SELECT color_code FROM projects WHERE projects.id = project_members.project_id) AS color,
|
||||
${valueString}
|
||||
FROM project_members
|
||||
WHERE team_member_id = $1`;
|
||||
const result = await db.query(q, [id]);
|
||||
|
||||
const obj: any[] = [];
|
||||
|
||||
result.rows.forEach((element: {
|
||||
project_id: string;
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
time_logged: number;
|
||||
}) => {
|
||||
obj.push({
|
||||
name: element.name,
|
||||
value: element.value || 1,
|
||||
colorValue: element.color + TEAM_MEMBER_TREE_MAP_COLOR_ALPHA,
|
||||
color: element.color + TEAM_MEMBER_TREE_MAP_COLOR_ALPHA,
|
||||
label: selected === "tasks" ? `${element.value} tasks` : formatDuration(moment.duration(element.value || "0", "seconds"))
|
||||
});
|
||||
});
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, obj));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async exportAllMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<void> {
|
||||
const {
|
||||
searchQuery,
|
||||
sortField,
|
||||
sortOrder,
|
||||
size,
|
||||
offset
|
||||
} = this.toPaginationOptions(req.query, ["tmiv.name", "tmiv.email", "u.name"]);
|
||||
const { start, end, project, status } = req.query;
|
||||
|
||||
const teamMembers = await this.getTeamMemberInsightData(req.user?.team_id, start || null, end, project, status, searchQuery, sortField, sortOrder, size, offset, req.query.all);
|
||||
|
||||
const exportDate = moment().format("MMM-DD-YYYY");
|
||||
const fileName = `Worklenz - Team Members Export - ${exportDate}`;
|
||||
const metadata = {};
|
||||
const title = "";
|
||||
|
||||
const workbook = new Excel.Workbook();
|
||||
const sheet = workbook.addWorksheet(title);
|
||||
|
||||
sheet.headerFooter = {
|
||||
firstHeader: title
|
||||
};
|
||||
|
||||
sheet.columns = [
|
||||
{ header: "Name", key: "name", width: 50 },
|
||||
{ header: "Task Count", key: "task_count", width: 25 },
|
||||
{ header: "Projects Count", key: "projects_count", width: 25 },
|
||||
{ header: "Email", key: "email", width: 40 },
|
||||
];
|
||||
|
||||
sheet.getCell("A1").value = req.user?.team_name;
|
||||
sheet.mergeCells("A1:D1");
|
||||
sheet.getCell("A1").alignment = { horizontal: "center" };
|
||||
|
||||
sheet.getCell("A2").value = `Exported on (${exportDate})`;
|
||||
sheet.getCell("A2").alignment = { horizontal: "center" };
|
||||
|
||||
sheet.getCell("A3").value = `From ${start || "-"} to ${end || "-"}`;
|
||||
|
||||
sheet.getRow(5).values = [
|
||||
"Name",
|
||||
"Task Count",
|
||||
"Projects Count",
|
||||
"Email"
|
||||
];
|
||||
|
||||
for (const item of teamMembers.data) {
|
||||
const data = {
|
||||
name: item.name,
|
||||
task_count: item.task_count,
|
||||
projects_count: item.projects_count,
|
||||
email: item.email
|
||||
};
|
||||
sheet.addRow(data);
|
||||
}
|
||||
|
||||
sheet.getCell("A1").style.fill = {
|
||||
type: "pattern",
|
||||
pattern: "solid",
|
||||
fgColor: { argb: "D9D9D9" }
|
||||
};
|
||||
sheet.getCell("A1").font = {
|
||||
size: 16
|
||||
};
|
||||
|
||||
sheet.getCell("A2").style.fill = {
|
||||
type: "pattern",
|
||||
pattern: "solid",
|
||||
fgColor: { argb: "F2F2F2" }
|
||||
};
|
||||
sheet.getCell("A2").font = {
|
||||
size: 12
|
||||
};
|
||||
|
||||
sheet.getRow(5).font = {
|
||||
bold: true
|
||||
};
|
||||
|
||||
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 exportByMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<void> {
|
||||
const exportDate = moment().format("MMM-DD-YYYY");
|
||||
const fileName = `Team Members - ${exportDate}`;
|
||||
const title = "";
|
||||
|
||||
const workbook = new Excel.Workbook();
|
||||
|
||||
workbook.addWorksheet(title);
|
||||
|
||||
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 toggleMemberActiveStatus(req: IWorkLenzRequest, res: IWorkLenzResponse) {
|
||||
if (!req.user?.team_id) return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
|
||||
|
||||
const q1 = `SELECT active FROM team_members WHERE id = $1;`;
|
||||
const result1 = await db.query(q1, [req.params?.id]);
|
||||
const [status] = result1.rows;
|
||||
|
||||
if (status.active) {
|
||||
const updateQ1 = `UPDATE users
|
||||
SET active_team = (SELECT id FROM teams WHERE user_id = users.id ORDER BY created_at DESC LIMIT 1)
|
||||
WHERE id = (SELECT user_id FROM team_members WHERE id = $1 AND active IS TRUE LIMIT 1);`;
|
||||
await db.query(updateQ1, [req.params?.id]);
|
||||
}
|
||||
|
||||
const q = `UPDATE team_members SET active = NOT active WHERE id = $1 RETURNING active;`;
|
||||
const result = await db.query(q, [req.params?.id]);
|
||||
const [data] = result.rows;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, [], `Team member ${data.active ? " activated" : " deactivated"} successfully.`));
|
||||
}
|
||||
|
||||
@HandleExceptions({
|
||||
raisedExceptions: {
|
||||
"ERROR_EMAIL_INVITATION_EXISTS": `Team member with email "{0}" already exists.`
|
||||
}
|
||||
})
|
||||
public static async addTeamMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
req.body.team_id = req.params?.id || null;
|
||||
|
||||
if (!req.body.team_id || !req.user?.id) return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
|
||||
|
||||
const newMembers = await this.createOrInviteMembers(req.body, req.user);
|
||||
return res.status(200).send(new ServerResponse(true, newMembers, `Your teammates will get an email that gives them access to your team.`).withTitle("Invitations sent"));
|
||||
}
|
||||
}
|
||||
104
worklenz-backend/src/controllers/teams-controller.ts
Normal file
104
worklenz-backend/src/controllers/teams-controller.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class TeamsController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { name } = req.body;
|
||||
|
||||
const checkAvailabilityq = `SELECT * from teams WHERE user_id = $2 AND name = $1`;
|
||||
const check = await db.query(checkAvailabilityq, [name, req.user?.id]);
|
||||
|
||||
if (check.rows.length) return res.status(200).send(new ServerResponse(false, null, "Team name already exist. Try anothor!"));
|
||||
|
||||
const q = `SELECT create_new_team($1, $2);`;
|
||||
const result = await db.query(q, [name, req.user?.id]);
|
||||
const [data] = result.rows;
|
||||
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,
|
||||
created_at,
|
||||
(id = $2) AS active,
|
||||
(user_id = $1) AS owner,
|
||||
EXISTS(SELECT 1
|
||||
FROM email_invitations
|
||||
WHERE team_id = teams.id
|
||||
AND team_member_id = (SELECT id
|
||||
FROM team_members
|
||||
WHERE team_members.user_id = $1
|
||||
AND team_members.team_id = teams.id)) AS pending_invitation,
|
||||
(CASE
|
||||
WHEN user_id = $1 THEN 'You'
|
||||
ELSE (SELECT name FROM users WHERE id = teams.user_id) END
|
||||
) AS owns_by
|
||||
FROM teams
|
||||
WHERE user_id = $1
|
||||
OR id IN (SELECT team_id FROM team_members WHERE team_members.user_id = $1
|
||||
AND team_members.active IS TRUE)
|
||||
ORDER BY name;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [req.user?.id, req.user?.team_id ?? null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTeamInvites(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT id,
|
||||
team_id,
|
||||
team_member_id,
|
||||
(SELECT name FROM teams WHERE id = team_id) AS team_name,
|
||||
(SELECT name FROM users WHERE id = (SELECT user_id FROM teams WHERE id = team_id)) AS team_owner
|
||||
FROM email_invitations
|
||||
WHERE email = (SELECT email FROM users WHERE id = $1);
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [req.user?.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT accept_invitation($1, $2, $3) AS invitation;`;
|
||||
const result = await db.query(q, [
|
||||
req.user?.email,
|
||||
req.body.team_member_id,
|
||||
req.user?.id,
|
||||
]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (req.body.show_alert) {
|
||||
return res.status(200).send(new ServerResponse(true, data.invitation, "Team invitation accepted"));
|
||||
}
|
||||
return res.status(200).send(new ServerResponse(true, data.invitation));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async activate(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT activate_team($1, $2)`;
|
||||
await db.query(q, [req.body.id, req.user?.id ?? null]);
|
||||
return res.status(200).send(new ServerResponse(true, { subdomain: null }));
|
||||
}
|
||||
|
||||
@HandleExceptions({
|
||||
raisedExceptions: {
|
||||
"TEAM_NAME_EXISTS_ERROR": "Team name already taken. Please enter a different name."
|
||||
}
|
||||
})
|
||||
public static async updateNameOnce(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT update_team_name_once($1, $2, $3);`;
|
||||
const result = await db.query(q, [req.user?.id, req.user?.team_id, req.body.name || null]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
23
worklenz-backend/src/controllers/timezones-controller.ts
Normal file
23
worklenz-backend/src/controllers/timezones-controller.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class TimezonesController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async get(_req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name, abbrev, utc_offset FROM timezones ORDER BY name;`;
|
||||
const result = await db.query(q, []);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `UPDATE users SET timezone_id = $2 WHERE id = $1;`;
|
||||
const result = await db.query(q, [req.user?.id, req.body.timezone]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows, "Timezone updated"));
|
||||
}
|
||||
}
|
||||
88
worklenz-backend/src/controllers/todo-list-controller.ts
Normal file
88
worklenz-backend/src/controllers/todo-list-controller.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
|
||||
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class TodoListController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
INSERT INTO personal_todo_list (name, description, color_code, user_id, index)
|
||||
VALUES ($1, $2, $3, $4, ((SELECT index FROM personal_todo_list ORDER BY index DESC LIMIT 1) + 1));
|
||||
`;
|
||||
const result = await db.query(q, [req.body.name, req.body.description, req.body.color_code, req.user?.id]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {searchQuery} = this.toPaginationOptions(req.query, ["name", "COALESCE(description, '')"]);
|
||||
const filterByDone = req.query.showCompleted ? "" : "AND done IS FALSE";
|
||||
const q = `
|
||||
SELECT id, name, description, color_code, done, created_at, updated_at
|
||||
FROM personal_todo_list
|
||||
WHERE user_id = $1 ${filterByDone} ${searchQuery}
|
||||
ORDER BY created_at DESC;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `UPDATE personal_todo_list SET done = $3 WHERE id = $1 AND user_id = $2;`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.id, !!req.body.done]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
UPDATE personal_todo_list
|
||||
SET done = $3,
|
||||
name = $4,
|
||||
description = $5,
|
||||
color_code = $6
|
||||
WHERE id = $1
|
||||
AND user_id = $2;
|
||||
`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.id, !!req.body.done, req.body.name, req.body.description, req.body.color_code]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateIndex(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const from = +(req.body.from || 0);
|
||||
const to = +(req.body.to || 0);
|
||||
|
||||
if (from === to)
|
||||
return res.status(200).send(new ServerResponse(true, []));
|
||||
|
||||
const q = `
|
||||
UPDATE personal_todo_list
|
||||
SET index=_index
|
||||
FROM (SELECT ROW_NUMBER() OVER (
|
||||
ORDER BY index < $2 DESC, index != $3 DESC, index >= $1 DESC, index
|
||||
) AS _index,
|
||||
user_id AS _user_id
|
||||
FROM personal_todo_list
|
||||
WHERE user_id = $1
|
||||
ORDER BY _user_id) AS _
|
||||
WHERE user_id = _user_id
|
||||
AND index != _index;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.id, from, to]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `DELETE FROM personal_todo_list WHERE id = $1 AND user_id = $2;`;
|
||||
const result = await db.query(q, [req.params.id, req.user?.id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
69
worklenz-backend/src/controllers/worklenz-controller-base.ts
Normal file
69
worklenz-backend/src/controllers/worklenz-controller-base.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { forEach } from "lodash";
|
||||
import {DEFAULT_PAGE_SIZE} from "../shared/constants";
|
||||
import {toTsQuery} from "../shared/utils";
|
||||
|
||||
export default abstract class WorklenzControllerBase {
|
||||
|
||||
protected static get paginatedDatasetDefaultStruct() {
|
||||
return {total: 0, data: []};
|
||||
}
|
||||
|
||||
protected static isValidHost(hostname: string) {
|
||||
return hostname === "worklenz.com"
|
||||
|| hostname === "www.worklenz.com"
|
||||
|| hostname === "dev.worklenz.com"
|
||||
|| hostname === "uat.worklenz.com";
|
||||
}
|
||||
|
||||
public static createTagList(list: Array<{ name?: string; end?: boolean; names?: string[]; }>, max = 4) {
|
||||
let data = [...(list || [])];
|
||||
if (data.length > max) {
|
||||
const remaining = list.slice(max);
|
||||
const names = remaining.map(i => i.name);
|
||||
data = data.slice(0, max);
|
||||
data.push({name: `+${remaining.length}`, end: true, names: names as string[]});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
protected static toPaginationOptions(queryParams: any, searchField: string | string[], isMemberFilter = false) {
|
||||
// Pagination
|
||||
const size = +(queryParams.size || DEFAULT_PAGE_SIZE);
|
||||
const index = +(queryParams.index || 1);
|
||||
const offset = queryParams.search ? 0 : (index - 1) * size;
|
||||
const paging = queryParams.paging || "true";
|
||||
|
||||
// let s = "";
|
||||
// if (typeof searchField === "string") {
|
||||
// s = `${searchField} || ' ' || id::TEXT`;
|
||||
// } else if (Array.isArray(searchField)) {
|
||||
// s = searchField.join(" || ' ' || ");
|
||||
// }
|
||||
|
||||
// const search = (queryParams.search as string || "").trim();
|
||||
// const searchQuery = search ? `AND TO_TSVECTOR(${s}) @@ TO_TSQUERY('${toTsQuery(search)}')` : "";
|
||||
|
||||
const search = (queryParams.search as string || "").trim();
|
||||
|
||||
let s = "";
|
||||
if (typeof searchField === "string") {
|
||||
s = ` ${searchField} ILIKE '%${search}%'`;
|
||||
} else if (Array.isArray(searchField)) {
|
||||
s = searchField.map(index => ` ${index} ILIKE '%${search}%'`).join(" OR ");
|
||||
}
|
||||
|
||||
let searchQuery = "";
|
||||
|
||||
if (search) {
|
||||
searchQuery = isMemberFilter ? ` (${s}) AND ` : ` AND (${s}) `;
|
||||
}
|
||||
|
||||
// Sort
|
||||
const sortField = /null|undefined/.test(queryParams.field as string) ? searchField : queryParams.field;
|
||||
const sortOrder = queryParams.order === "descend" ? "desc" : "asc";
|
||||
|
||||
return {searchQuery, sortField, sortOrder, size, offset, paging};
|
||||
}
|
||||
|
||||
}
|
||||
58
worklenz-backend/src/cron_jobs/daily-digest-job.ts
Normal file
58
worklenz-backend/src/cron_jobs/daily-digest-job.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {CronJob} from "cron";
|
||||
import moment from "moment";
|
||||
import db from "../config/db";
|
||||
import {IDailyDigest} from "../interfaces/daily-digest";
|
||||
import {sendDailyDigest} from "../shared/email-notifications";
|
||||
import {log_error} from "../shared/utils";
|
||||
import {getBaseUrl, mapTeams} from "./helpers";
|
||||
|
||||
// At 11:00+00 (4.30pm+530) on every day-of-month if it's on every day-of-week from Monday through Friday.
|
||||
const TIME = "0 11 */1 * 1-5";
|
||||
// const TIME = "0/30 * * * *";
|
||||
// const TIME = "* * * * *";
|
||||
|
||||
const log = (value: any) => console.log("daily-digest-cron-job:", value);
|
||||
|
||||
async function onDailyDigestJobTick() {
|
||||
try {
|
||||
log("(cron) Daily digest job started.");
|
||||
const q = "SELECT get_daily_digest() AS digest;";
|
||||
const result = await db.query(q, []);
|
||||
const [fn] = result.rows;
|
||||
|
||||
const dataset: IDailyDigest[] = fn.digest || [];
|
||||
|
||||
let sentCount = 0;
|
||||
|
||||
for (const digest of dataset) {
|
||||
digest.greeting = `Hi ${digest.name},`;
|
||||
digest.note = `Here's your ${moment().format("dddd")} update!`;
|
||||
digest.base_url = `${getBaseUrl()}/worklenz`;
|
||||
digest.settings_url = `${getBaseUrl()}/worklenz/settings/notifications`;
|
||||
|
||||
digest.recently_assigned = mapTeams(digest.recently_assigned);
|
||||
digest.overdue = mapTeams(digest.overdue);
|
||||
digest.recently_completed = mapTeams(digest.recently_completed);
|
||||
|
||||
if (digest.recently_assigned?.length || digest.overdue?.length || digest.recently_completed?.length) {
|
||||
sentCount++;
|
||||
void sendDailyDigest(digest.email as string, digest);
|
||||
}
|
||||
}
|
||||
log(`(cron) Daily digest job ended with ${sentCount} emails.`);
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
log("(cron) Daily digest job ended with errors.");
|
||||
}
|
||||
}
|
||||
|
||||
export function startDailyDigestJob() {
|
||||
log("(cron) Daily digest job ready.");
|
||||
const job = new CronJob(
|
||||
TIME,
|
||||
() => void onDailyDigestJobTick(),
|
||||
() => log("(cron) Daily Digest job successfully executed."),
|
||||
true
|
||||
);
|
||||
job.start();
|
||||
}
|
||||
75
worklenz-backend/src/cron_jobs/helpers.ts
Normal file
75
worklenz-backend/src/cron_jobs/helpers.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import {ITaskAssignmentModelProject, ITaskAssignmentModelTeam} from "../interfaces/task-assignments-model";
|
||||
import {isLocalServer} from "../shared/utils";
|
||||
|
||||
export function mapMembersWithAnd(members: string) {
|
||||
const $members = members.split(",").map(m => m.trim());
|
||||
if ($members.length > 1) {
|
||||
const last = $members.pop();
|
||||
const end = last ? ` and ${last}` : "";
|
||||
return `${$members.join(", ")}${end}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function getBaseUrl() {
|
||||
if (isLocalServer()) return `http://${process.env.HOSTNAME}`;
|
||||
return `https://${process.env.HOSTNAME}`;
|
||||
}
|
||||
|
||||
function mapMembers(project: ITaskAssignmentModelProject) {
|
||||
for (const task of project.tasks || []) {
|
||||
if (task.members)
|
||||
task.members = mapMembersWithAnd(task.members);
|
||||
}
|
||||
}
|
||||
|
||||
function updateUrls(project: ITaskAssignmentModelProject) {
|
||||
project.url = `${getBaseUrl()}/worklenz/projects/${project.id}`;
|
||||
if (project.tasks) {
|
||||
project.tasks = project.tasks.map(task => {
|
||||
if (task.id)
|
||||
task.url = `${project.url}?task=${task.id}`;
|
||||
return task;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function mapTeams(data?: ITaskAssignmentModelTeam[]) {
|
||||
if (!data) return [];
|
||||
|
||||
const result = [];
|
||||
for (const item of data || []) {
|
||||
const projects = item.projects?.filter(project => project.tasks?.length);
|
||||
for (const project of projects || []) {
|
||||
if (project.id) {
|
||||
mapMembers(project);
|
||||
updateUrls(project);
|
||||
}
|
||||
}
|
||||
|
||||
if (projects?.length) {
|
||||
item.projects = projects;
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
export function mapProjects(data?: ITaskAssignmentModelTeam[]) {
|
||||
if (!data) return [];
|
||||
|
||||
const result = [];
|
||||
for (const item of data || []) {
|
||||
const projects = item.projects?.filter(project => project.tasks?.length);
|
||||
for (const project of projects || []) {
|
||||
if (project.id) {
|
||||
mapMembers(project);
|
||||
updateUrls(project);
|
||||
result.push(project);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
9
worklenz-backend/src/cron_jobs/index.ts
Normal file
9
worklenz-backend/src/cron_jobs/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {startDailyDigestJob} from "./daily-digest-job";
|
||||
import {startNotificationsJob} from "./notifications-job";
|
||||
import {startProjectDigestJob} from "./project-digest-job";
|
||||
|
||||
export function startCronJobs() {
|
||||
startNotificationsJob();
|
||||
startDailyDigestJob();
|
||||
startProjectDigestJob();
|
||||
}
|
||||
68
worklenz-backend/src/cron_jobs/notifications-job.ts
Normal file
68
worklenz-backend/src/cron_jobs/notifications-job.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// https://www.npmjs.com/package/cron
|
||||
// https://crontab.guru/#0_22_*/1_*_*
|
||||
|
||||
import {CronJob} from "cron";
|
||||
import db from "../config/db";
|
||||
import {ITaskAssignmentsModel} from "../interfaces/task-assignments-model";
|
||||
import {sendAssignmentUpdate} from "../shared/email-notifications";
|
||||
import {log_error} from "../shared/utils";
|
||||
import {getBaseUrl, mapProjects} from "./helpers";
|
||||
|
||||
const TIME = "*/10 * * * *";
|
||||
|
||||
const log = (value: any) => console.log("notifications-cron-job:", value);
|
||||
|
||||
function getModel(model: ITaskAssignmentsModel): ITaskAssignmentsModel {
|
||||
const mappedModel: ITaskAssignmentsModel = {...model};
|
||||
|
||||
mappedModel.name = mappedModel.name?.split(" ")[0] || "";
|
||||
mappedModel.url = `${getBaseUrl()}/worklenz/team/member/${mappedModel.team_member_id}`;
|
||||
mappedModel.settings_url = `${getBaseUrl()}/worklenz/settings/notifications`;
|
||||
|
||||
const teams = [];
|
||||
for (const team of model.teams || []) {
|
||||
team.projects = mapProjects([team]);
|
||||
if (team.projects.length)
|
||||
teams.push(team);
|
||||
}
|
||||
|
||||
mappedModel.teams = teams;
|
||||
return mappedModel;
|
||||
}
|
||||
|
||||
async function onNotificationJobTick() {
|
||||
try {
|
||||
log("(cron) Notifications job started.");
|
||||
const q = "SELECT get_task_updates() AS updates;";
|
||||
const result = await db.query(q, []);
|
||||
const [data] = result.rows;
|
||||
const updates = (data.updates || []) as ITaskAssignmentsModel[];
|
||||
|
||||
let sentCount = 0;
|
||||
|
||||
for (const item of updates) {
|
||||
if (item.email) {
|
||||
const model = getModel(item);
|
||||
if (model.teams?.length) {
|
||||
sentCount++;
|
||||
void sendAssignmentUpdate(item.email, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
log(`(cron) Notifications job ended with ${sentCount} emails.`);
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
log("(cron) Notifications job ended with errors.");
|
||||
}
|
||||
}
|
||||
|
||||
export function startNotificationsJob() {
|
||||
log("(cron) Email notifications job ready.");
|
||||
const job = new CronJob(
|
||||
TIME,
|
||||
() => void onNotificationJobTick(),
|
||||
() => log("(cron) Notifications job successfully executed."),
|
||||
true
|
||||
);
|
||||
job.start();
|
||||
}
|
||||
70
worklenz-backend/src/cron_jobs/project-digest-job.ts
Normal file
70
worklenz-backend/src/cron_jobs/project-digest-job.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {CronJob} from "cron";
|
||||
import db from "../config/db";
|
||||
import {log_error} from "../shared/utils";
|
||||
import {getBaseUrl} from "./helpers";
|
||||
import {IProjectDigest, IProjectDigestTask} from "../interfaces/project-digest";
|
||||
import {sendProjectDailyDigest} from "../shared/email-notifications";
|
||||
|
||||
// At 11:00+00 (4.30pm+530) on every day-of-month if it's on every day-of-week from Monday through Friday.
|
||||
const TIME = "0 11 */1 * 1-5";
|
||||
// const TIME = "* * * * *";
|
||||
|
||||
const log = (value: any) => console.log("project-digest-cron-job:", value);
|
||||
|
||||
function updateTaskUrls(projectId: string, tasks: IProjectDigestTask[]) {
|
||||
const baseUrl = getBaseUrl();
|
||||
for (const task of tasks) {
|
||||
task.url = `${baseUrl}/worklenz/projects/${projectId}?tab=tasks-list&task=${task.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateMetadata(project: IProjectDigest, subscriberName: string) {
|
||||
project.greeting = `Hi ${subscriberName},`;
|
||||
project.summary = `Here's the "${project.name}" summary | ${project.team_name}`;
|
||||
project.settings_url = `${getBaseUrl()}/worklenz/settings/notifications`;
|
||||
project.project_url = `${getBaseUrl()}/worklenz/projects/${project.id}?tab=tasks-list`;
|
||||
}
|
||||
|
||||
async function onProjectDigestJobTick() {
|
||||
try {
|
||||
log("(cron) Daily digest job started.");
|
||||
const q = "SELECT get_project_daily_digest() AS digest;";
|
||||
const result = await db.query(q, []);
|
||||
const [fn] = result.rows;
|
||||
|
||||
const dataset: IProjectDigest[] = fn.digest || [];
|
||||
|
||||
let sentCount = 0;
|
||||
|
||||
for (const project of dataset) {
|
||||
for (const subscriber of project.subscribers) {
|
||||
updateMetadata(project, subscriber.name);
|
||||
|
||||
updateTaskUrls(project.id, project.today_completed);
|
||||
updateTaskUrls(project.id, project.today_new);
|
||||
updateTaskUrls(project.id, project.due_tomorrow);
|
||||
|
||||
if (subscriber.email) {
|
||||
sentCount++;
|
||||
void sendProjectDailyDigest(subscriber.email, project);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log(`(cron) Project digest job ended with ${sentCount} emails.`);
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
log("(cron) Project digest job ended with errors.");
|
||||
}
|
||||
}
|
||||
|
||||
export function startProjectDigestJob() {
|
||||
log("(cron) Project digest job ready.");
|
||||
const job = new CronJob(
|
||||
TIME,
|
||||
() => void onProjectDigestJobTick(),
|
||||
() => log("(cron) Project Digest job successfully executed."),
|
||||
true
|
||||
);
|
||||
job.start();
|
||||
}
|
||||
83
worklenz-backend/src/decorators/handle-exceptions.ts
Normal file
83
worklenz-backend/src/decorators/handle-exceptions.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import WorklenzControllerBase from "../controllers/worklenz-controller-base";
|
||||
import {ServerResponse} from "../models/server-response";
|
||||
import {DEFAULT_ERROR_MESSAGE} from "../shared/constants";
|
||||
import {DB_CONSTRAINS} from "../shared/constraints";
|
||||
import {log_error} from "../shared/utils";
|
||||
import {Response} from "express";
|
||||
|
||||
interface IExceptionHandlerConfig {
|
||||
message?: string;
|
||||
/** e.g. req.user? "user", req.body? "body" */
|
||||
logWithError?: string;
|
||||
/** Throws from postgres functions */
|
||||
raisedExceptions?: { [x: string]: string };
|
||||
}
|
||||
|
||||
const defaults: IExceptionHandlerConfig = {
|
||||
message: DEFAULT_ERROR_MESSAGE,
|
||||
raisedExceptions: {},
|
||||
logWithError: "user"
|
||||
};
|
||||
|
||||
const isValid = (options: any, key: string) => Object.keys(options[key] || {}).length > 0;
|
||||
const mergeWithDefaults = (options: any) => ({...defaults, ...(options || {})});
|
||||
|
||||
function getConstraint(error: any) {
|
||||
return DB_CONSTRAINS[error?.constraint] ?? null;
|
||||
}
|
||||
|
||||
function getConstraintResponse(constraint: string) {
|
||||
if (constraint === "[IGNORE]")
|
||||
return new ServerResponse(true, null);
|
||||
return new ServerResponse(false, null, constraint || DEFAULT_ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
function hasRaisedException(opt: any, keys: any[]): boolean {
|
||||
return opt.raisedExceptions?.[keys[0]];
|
||||
}
|
||||
|
||||
function getExceptionMessage(opt: any, keys: any[]) {
|
||||
return (opt.raisedExceptions[keys[0]] || DEFAULT_ERROR_MESSAGE).replace(/(\{0\})/g, (keys[1] || ""));
|
||||
}
|
||||
|
||||
function getKeys(error: any) {
|
||||
return ((error?.message) || "").split(":");
|
||||
}
|
||||
|
||||
function handleError(error: any, res: Response, opt: any, req: any) {
|
||||
const constraint = getConstraint(error);
|
||||
if (typeof constraint === "string") {
|
||||
const response = getConstraintResponse(constraint);
|
||||
return res.status(200).send(response);
|
||||
}
|
||||
|
||||
if (isValid(opt, "raisedExceptions")) {
|
||||
const keys = getKeys(error);
|
||||
if (hasRaisedException(opt, keys)) {
|
||||
const msg = getExceptionMessage(opt, keys);
|
||||
return res.status(200).send(new ServerResponse(false, null, msg));
|
||||
}
|
||||
}
|
||||
|
||||
log_error(error, (opt.logWithError && req[opt.logWithError]) || null);
|
||||
return res.status(200).send(new ServerResponse(false, null, opt.message));
|
||||
}
|
||||
|
||||
/** HandleExceptions can only be used with an instance of WorklenzControllerBase. */
|
||||
export default function HandleExceptions(options?: IExceptionHandlerConfig) {
|
||||
const opt = mergeWithDefaults(options);
|
||||
return (target: any, key: string, descriptor: PropertyDescriptor) => {
|
||||
if (!(target.prototype instanceof WorklenzControllerBase))
|
||||
throw new Error("@HandleExceptions can only be used with an instance of WorklenzControllerBase.");
|
||||
|
||||
const originalMethod = descriptor.value;
|
||||
descriptor.value = async (...args: any[]) => {
|
||||
try {
|
||||
return await originalMethod.apply(target, args);
|
||||
} catch (error: any) {
|
||||
const [req, res] = args;
|
||||
return handleError(error, res, opt, req);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
0
worklenz-backend/src/interfaces/.gitkeep
Normal file
0
worklenz-backend/src/interfaces/.gitkeep
Normal file
@@ -0,0 +1,59 @@
|
||||
export interface ISESBouncedRecipient {
|
||||
emailAddress: string;
|
||||
action: string;
|
||||
status: string;
|
||||
diagnosticCode: string;
|
||||
}
|
||||
|
||||
export interface ISESBounce {
|
||||
feedbackId: string;
|
||||
bounceType: string;
|
||||
bounceSubType: string;
|
||||
bouncedRecipients: ISESBouncedRecipient[];
|
||||
timestamp: string;
|
||||
remoteMtaIp: string;
|
||||
reportingMTA: string;
|
||||
}
|
||||
|
||||
export interface ISESBouncedHeaders {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ISESCommonHeaders {
|
||||
from: string[];
|
||||
to: string[];
|
||||
subject: string;
|
||||
}
|
||||
|
||||
export interface ISESBouncedMail {
|
||||
timestamp: string;
|
||||
source: string;
|
||||
sourceArn: string;
|
||||
sourceIp: string;
|
||||
callerIdentity: string;
|
||||
sendingAccountId: string;
|
||||
messageId: string;
|
||||
destination: string[];
|
||||
headersTruncated: boolean;
|
||||
headers: ISESBouncedHeaders[];
|
||||
commonHeaders: ISESCommonHeaders;
|
||||
}
|
||||
|
||||
export interface ISESBouncedMessage {
|
||||
notificationType: string;
|
||||
bounce: ISESBounce;
|
||||
mail: ISESBouncedMail;
|
||||
}
|
||||
|
||||
export interface ISESBouncedEmailResponse {
|
||||
Type: string;
|
||||
MessageId: string;
|
||||
TopicArn: string;
|
||||
Message: ISESBouncedMessage;
|
||||
Timestamp: string;
|
||||
SignatureVersion: string;
|
||||
Signature: string;
|
||||
SigningCertURL: string;
|
||||
UnsubscribeURL: string;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
export interface ISESComplaintMail {
|
||||
timestamp: string;
|
||||
source: string;
|
||||
sourceArn: string;
|
||||
sourceIp: string;
|
||||
callerIdentity: string;
|
||||
sendingAccountId: string;
|
||||
messageId: string;
|
||||
destination: string[];
|
||||
}
|
||||
|
||||
export interface ISESComplaintEmailAddress {
|
||||
emailAddress: string;
|
||||
}
|
||||
|
||||
export interface ISESComplaint {
|
||||
feedbackId: string;
|
||||
complaintSubType: string | null;
|
||||
complainedRecipients: ISESComplaintEmailAddress[];
|
||||
timestamp: string;
|
||||
userAgent: string;
|
||||
complaintFeedbackType: string;
|
||||
arrivalDate: string;
|
||||
}
|
||||
|
||||
export interface ISESComplaintMessage {
|
||||
notificationType: string;
|
||||
complaint: ISESComplaint;
|
||||
mail: ISESComplaintMail;
|
||||
}
|
||||
|
||||
export interface ISESComplaintResponse {
|
||||
Type: string;
|
||||
MessageId: string;
|
||||
TopicArn: string;
|
||||
Message: ISESComplaintMessage;
|
||||
Timestamp: string;
|
||||
SignatureVersion: string;
|
||||
Signature: string;
|
||||
SigningCertURL: string;
|
||||
UnsubscribeURL: string;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export interface ICommentEmailNotification {
|
||||
greeting: string;
|
||||
summary: string;
|
||||
team: string;
|
||||
project_name: string;
|
||||
comment: string;
|
||||
task: string;
|
||||
settings_url: string;
|
||||
task_url: string;
|
||||
}
|
||||
|
||||
export interface IProjectCommentEmailNotification {
|
||||
greeting: string;
|
||||
summary: string;
|
||||
team: string;
|
||||
project_name: string;
|
||||
comment: string;
|
||||
}
|
||||
13
worklenz-backend/src/interfaces/daily-digest.ts
Normal file
13
worklenz-backend/src/interfaces/daily-digest.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {ITaskAssignmentModelTeam} from "./task-assignments-model";
|
||||
|
||||
export interface IDailyDigest {
|
||||
name?: string;
|
||||
greeting?: string;
|
||||
note?: string;
|
||||
email?: string;
|
||||
base_url?: string;
|
||||
settings_url?: string;
|
||||
recently_assigned?: ITaskAssignmentModelTeam[];
|
||||
overdue?: ITaskAssignmentModelTeam[];
|
||||
recently_completed?: ITaskAssignmentModelTeam[];
|
||||
}
|
||||
5
worklenz-backend/src/interfaces/deserialize-callback.ts
Normal file
5
worklenz-backend/src/interfaces/deserialize-callback.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import {IPassportSession} from "./passport-session";
|
||||
|
||||
export interface IDeserializeCallback {
|
||||
(error: unknown | null, user: IPassportSession | null): void;
|
||||
}
|
||||
16
worklenz-backend/src/interfaces/email-template-type.ts
Normal file
16
worklenz-backend/src/interfaces/email-template-type.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export enum IEmailTemplateType {
|
||||
None,
|
||||
NewSubscriber,
|
||||
TeamMemberInvitation,
|
||||
UnregisteredTeamMemberInvitation,
|
||||
PasswordChange,
|
||||
Welcome,
|
||||
OTPVerification,
|
||||
ResetPassword,
|
||||
TaskAssigneeChange,
|
||||
DailyDigest,
|
||||
TaskDone,
|
||||
ProjectDailyDigest,
|
||||
TaskComment,
|
||||
ProjectComment
|
||||
}
|
||||
24
worklenz-backend/src/interfaces/gantt-chart.ts
Normal file
24
worklenz-backend/src/interfaces/gantt-chart.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface IGanttDateRange {
|
||||
isSunday?: boolean;
|
||||
isToday?: boolean;
|
||||
isWeekend?: boolean;
|
||||
isLastDayOfWeek?: boolean;
|
||||
isLastDayOfMonth?: boolean;
|
||||
date?: Date;
|
||||
}
|
||||
|
||||
export interface IGanttWeekRange {
|
||||
max?: number;
|
||||
min?: number;
|
||||
month_name?: string;
|
||||
week_index?: number;
|
||||
days_of_week?: IGanttDateRange[];
|
||||
}
|
||||
|
||||
export interface IGanttMonthRange {
|
||||
max?: number;
|
||||
min?: number;
|
||||
month_name?: string;
|
||||
month_index?: number;
|
||||
days_of_month?: IGanttDateRange[];
|
||||
}
|
||||
20
worklenz-backend/src/interfaces/passport-session.ts
Normal file
20
worklenz-backend/src/interfaces/passport-session.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {IUser} from "./user";
|
||||
|
||||
export interface IPassportSession extends IUser {
|
||||
id?: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
owner?: boolean;
|
||||
team_id?: string;
|
||||
team_member_id?: string;
|
||||
team_name?: string;
|
||||
is_admin?: boolean;
|
||||
is_member?: boolean;
|
||||
is_google?: boolean;
|
||||
build_v?: string;
|
||||
timezone?: string;
|
||||
timezone_name?: string;
|
||||
socket_id?: string;
|
||||
is_expired?: boolean;
|
||||
owner_id?: string;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface IPasswordValidityResult {
|
||||
contains: ("lowercase" | "uppercase" | "number" | "symbol")[];
|
||||
length: number;
|
||||
value: number;
|
||||
text: "Too Weak" | "Weak" | "Strong" | "Excellent";
|
||||
}
|
||||
12
worklenz-backend/src/interfaces/pg-session-data.ts
Normal file
12
worklenz-backend/src/interfaces/pg-session-data.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface PgSessionData {
|
||||
cookie: {
|
||||
originalMaxAge: number;
|
||||
expires: string;
|
||||
httpOnly: boolean;
|
||||
path: string;
|
||||
};
|
||||
flash: object;
|
||||
passport?: {
|
||||
user: string;
|
||||
};
|
||||
}
|
||||
9
worklenz-backend/src/interfaces/project-category.ts
Normal file
9
worklenz-backend/src/interfaces/project-category.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface IProjectCategory {
|
||||
id?: string;
|
||||
name?: string;
|
||||
color_code?: string;
|
||||
team_id?: string;
|
||||
created_by?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
25
worklenz-backend/src/interfaces/project-digest.ts
Normal file
25
worklenz-backend/src/interfaces/project-digest.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export interface IProjectDigestTask {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
members: string;
|
||||
}
|
||||
|
||||
export interface IProjectDigestSubscriber {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface IProjectDigest {
|
||||
id: string;
|
||||
name: string;
|
||||
team_name: string;
|
||||
greeting: string;
|
||||
summary: string;
|
||||
settings_url: string;
|
||||
project_url: string;
|
||||
today_completed: IProjectDigestTask[];
|
||||
today_new: IProjectDigestTask[];
|
||||
due_tomorrow: IProjectDigestTask[];
|
||||
subscribers: IProjectDigestSubscriber[];
|
||||
}
|
||||
11
worklenz-backend/src/interfaces/project-folder.ts
Normal file
11
worklenz-backend/src/interfaces/project-folder.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface IProjectFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
key: string;
|
||||
color_code: string;
|
||||
created_by: string;
|
||||
parent_folder_id?: string;
|
||||
team_id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
3
worklenz-backend/src/interfaces/serialize-callback.ts
Normal file
3
worklenz-backend/src/interfaces/serialize-callback.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface ISerializeCallback {
|
||||
(error: string | null, id: string | null): void;
|
||||
}
|
||||
5
worklenz-backend/src/interfaces/socket-session.ts
Normal file
5
worklenz-backend/src/interfaces/socket-session.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface ISocketSession {
|
||||
session?: {
|
||||
passport?: { user?: string; }
|
||||
}
|
||||
}
|
||||
30
worklenz-backend/src/interfaces/task-assignments-model.ts
Normal file
30
worklenz-backend/src/interfaces/task-assignments-model.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export interface ITaskAssignmentModelTask {
|
||||
id?: string;
|
||||
task_name?: string;
|
||||
updater_name?: string;
|
||||
members?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface ITaskAssignmentModelProject {
|
||||
id?: string;
|
||||
name?: string;
|
||||
url?: string;
|
||||
tasks?: ITaskAssignmentModelTask[];
|
||||
}
|
||||
|
||||
export interface ITaskAssignmentModelTeam {
|
||||
id?: string;
|
||||
name?: string;
|
||||
team_member_id?: string;
|
||||
projects?: ITaskAssignmentModelProject[];
|
||||
}
|
||||
|
||||
export interface ITaskAssignmentsModel {
|
||||
email?: string;
|
||||
name?: string;
|
||||
team_member_id?: string;
|
||||
url?: string;
|
||||
settings_url?: string;
|
||||
teams?: ITaskAssignmentModelTeam[];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user