Initial commit: Angular frontend and Expressjs backend
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import db from "../../config/db";
|
||||
import { IActivityLog, IActivityLogAttributeTypes, IActivityLogChangeType } from "./interfaces";
|
||||
import { log_error } from "../../shared/utils";
|
||||
import moment from "moment";
|
||||
import { getLoggedInUserIdFromSocket } from "../../socket.io/util";
|
||||
|
||||
export async function insertToActivityLogs(activityLog: IActivityLog) {
|
||||
try {
|
||||
const {
|
||||
task_id,
|
||||
attribute_type,
|
||||
user_id,
|
||||
log_type,
|
||||
old_value,
|
||||
new_value,
|
||||
next_string
|
||||
} = activityLog;
|
||||
|
||||
const q = `
|
||||
INSERT INTO task_activity_logs (task_id, team_id, attribute_type, user_id, log_type, old_value, new_value, next_string, project_id)
|
||||
VALUES (
|
||||
$1,
|
||||
(SELECT team_id FROM projects WHERE id = (SELECT project_id FROM tasks WHERE tasks.id = $1)),
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
(SELECT project_id FROM tasks WHERE tasks.id = $1));
|
||||
`;
|
||||
await db.query(q, [task_id, attribute_type, user_id, log_type, old_value, new_value, next_string]);
|
||||
} catch (e) {
|
||||
log_error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTaskDetails(task_id: string, column: string) {
|
||||
try {
|
||||
const q = `SELECT ${column}
|
||||
FROM tasks
|
||||
WHERE id = $1;`;
|
||||
const result = await db.query(q, [task_id]);
|
||||
const [data] = result.rows;
|
||||
return data;
|
||||
} catch (e) {
|
||||
log_error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTaskPhaseDetails(task_id: string) {
|
||||
try {
|
||||
const q = `SELECT phase_id FROM task_phase WHERE task_id = $1`;
|
||||
const result = await db.query(q, [task_id]);
|
||||
const [data] = result.rows;
|
||||
return data ? data : {phase_id: null};
|
||||
} catch (e) {
|
||||
log_error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logStartDateChange(activityLog: IActivityLog) {
|
||||
const { task_id, new_value, old_value } = activityLog;
|
||||
|
||||
if (!task_id || !activityLog.socket) return;
|
||||
if (!(moment(old_value).isSame(moment(new_value), "date"))) {
|
||||
activityLog.user_id = getLoggedInUserIdFromSocket(activityLog.socket);
|
||||
activityLog.log_type = IActivityLogChangeType.UPDATE;
|
||||
activityLog.attribute_type = IActivityLogAttributeTypes.START_DATE;
|
||||
activityLog.new_value = activityLog.new_value ? moment(activityLog.new_value).format("YYYY-MM-DD") : null;
|
||||
activityLog.old_value = activityLog.old_value ? moment(activityLog.old_value).format("YYYY-MM-DD") : null;
|
||||
|
||||
insertToActivityLogs(activityLog);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logEndDateChange(activityLog: IActivityLog) {
|
||||
const { task_id, new_value, old_value } = activityLog;
|
||||
|
||||
if (!task_id || !activityLog.socket) return;
|
||||
if (!(moment(old_value).isSame(moment(new_value), "date"))) {
|
||||
activityLog.user_id = getLoggedInUserIdFromSocket(activityLog.socket);
|
||||
activityLog.log_type = IActivityLogChangeType.UPDATE;
|
||||
activityLog.attribute_type = IActivityLogAttributeTypes.END_DATE;
|
||||
activityLog.new_value = activityLog.new_value ? moment(activityLog.new_value).format("YYYY-MM-DD") : null;
|
||||
activityLog.old_value = activityLog.old_value ? moment(activityLog.old_value).format("YYYY-MM-DD") : null;
|
||||
|
||||
insertToActivityLogs(activityLog);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logNameChange(activityLog: IActivityLog) {
|
||||
const { task_id, new_value, old_value } = activityLog;
|
||||
|
||||
if (!task_id || !activityLog.socket) return;
|
||||
if (old_value !== new_value) {
|
||||
activityLog.user_id = getLoggedInUserIdFromSocket(activityLog.socket);
|
||||
activityLog.log_type = IActivityLogChangeType.UPDATE;
|
||||
activityLog.attribute_type = IActivityLogAttributeTypes.NAME;
|
||||
|
||||
insertToActivityLogs(activityLog);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logTotalMinutes(activityLog: IActivityLog) {
|
||||
const { task_id, new_value, old_value } = activityLog;
|
||||
|
||||
if (!task_id || !activityLog.socket) return;
|
||||
if (old_value !== new_value) {
|
||||
activityLog.user_id = getLoggedInUserIdFromSocket(activityLog.socket);
|
||||
activityLog.log_type = IActivityLogChangeType.UPDATE;
|
||||
activityLog.attribute_type = IActivityLogAttributeTypes.ESTIMATION;
|
||||
|
||||
insertToActivityLogs(activityLog);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logMemberAssignment(activityLog: IActivityLog) {
|
||||
const { task_id, new_value, old_value } = activityLog;
|
||||
|
||||
const q = `SELECT user_id, name
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_id = $1;`;
|
||||
const result = await db.query(q, [new_value]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (!task_id || !activityLog.socket) return;
|
||||
if (old_value !== new_value) {
|
||||
activityLog.new_value = data.user_id || null;
|
||||
activityLog.user_id = getLoggedInUserIdFromSocket(activityLog.socket);
|
||||
activityLog.log_type = activityLog.assign_type === "ASSIGN" ? IActivityLogChangeType.ASSIGN : IActivityLogChangeType.UNASSIGN;
|
||||
activityLog.attribute_type = IActivityLogAttributeTypes.ASSIGNEES;
|
||||
activityLog.next_string = data.name || null;
|
||||
|
||||
insertToActivityLogs(activityLog);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logLabelsUpdate(activityLog: IActivityLog) {
|
||||
const { task_id, new_value, old_value } = activityLog;
|
||||
|
||||
const q = `SELECT EXISTS(SELECT task_id FROM task_labels WHERE task_id = $1 AND label_id = $2)`;
|
||||
const result = await db.query(q, [task_id, new_value]);
|
||||
const [data] = result.rows;
|
||||
activityLog.log_type = data.exists ? IActivityLogChangeType.CREATE : IActivityLogChangeType.DELETE;
|
||||
|
||||
if (!task_id || !activityLog.socket) return;
|
||||
if (old_value !== new_value) {
|
||||
activityLog.user_id = getLoggedInUserIdFromSocket(activityLog.socket);
|
||||
activityLog.attribute_type = IActivityLogAttributeTypes.LABEL;
|
||||
|
||||
insertToActivityLogs(activityLog);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logStatusChange(activityLog: IActivityLog) {
|
||||
const { task_id, new_value, old_value } = activityLog;
|
||||
|
||||
if (!task_id || !activityLog.socket) return;
|
||||
if (old_value !== new_value) {
|
||||
activityLog.user_id = getLoggedInUserIdFromSocket(activityLog.socket);
|
||||
activityLog.attribute_type = IActivityLogAttributeTypes.STATUS;
|
||||
activityLog.log_type = IActivityLogChangeType.UPDATE;
|
||||
|
||||
insertToActivityLogs(activityLog);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logPriorityChange(activityLog: IActivityLog) {
|
||||
const { task_id, new_value, old_value } = activityLog;
|
||||
|
||||
if (!task_id || !activityLog.socket) return;
|
||||
if (old_value !== new_value) {
|
||||
activityLog.user_id = getLoggedInUserIdFromSocket(activityLog.socket);
|
||||
activityLog.attribute_type = IActivityLogAttributeTypes.PRIORITY;
|
||||
activityLog.log_type = IActivityLogChangeType.UPDATE;
|
||||
|
||||
insertToActivityLogs(activityLog);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logDescriptionChange(activityLog: IActivityLog) {
|
||||
const { task_id, new_value, old_value } = activityLog;
|
||||
|
||||
if (!task_id || !activityLog.socket) return;
|
||||
if (old_value !== new_value) {
|
||||
activityLog.user_id = getLoggedInUserIdFromSocket(activityLog.socket);
|
||||
activityLog.attribute_type = IActivityLogAttributeTypes.DESCRIPTION;
|
||||
activityLog.log_type = IActivityLogChangeType.UPDATE;
|
||||
|
||||
insertToActivityLogs(activityLog);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logPhaseChange(activityLog: IActivityLog) {
|
||||
const { task_id, new_value, old_value } = activityLog;
|
||||
if (!task_id || !activityLog.socket) return;
|
||||
|
||||
if (old_value !== new_value) {
|
||||
activityLog.user_id = getLoggedInUserIdFromSocket(activityLog.socket);
|
||||
activityLog.attribute_type = IActivityLogAttributeTypes.PHASE;
|
||||
activityLog.log_type = IActivityLogChangeType.UPDATE;
|
||||
|
||||
insertToActivityLogs(activityLog);
|
||||
}
|
||||
}
|
||||
40
worklenz-backend/src/services/activity-logs/interfaces.ts
Normal file
40
worklenz-backend/src/services/activity-logs/interfaces.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Socket } from "socket.io";
|
||||
|
||||
export interface IActivityLog {
|
||||
task_id?: string;
|
||||
team_id?: string;
|
||||
attribute_type?: string;
|
||||
user_id?: string | null;
|
||||
log_type?: string;
|
||||
old_value?: string | null;
|
||||
new_value?: string | null;
|
||||
assign_type?: string | null;
|
||||
change_type?: string | null;
|
||||
socket?: Socket;
|
||||
prev_string?: string | null;
|
||||
next_string?: string | null;
|
||||
}
|
||||
|
||||
export enum IActivityLogAttributeTypes {
|
||||
NAME = "name",
|
||||
STATUS = "status",
|
||||
ASSIGNEES = "assignee",
|
||||
END_DATE = "end_date",
|
||||
START_DATE = "start_date",
|
||||
PRIORITY = "priority",
|
||||
ESTIMATION = "estimation",
|
||||
LABEL = "label",
|
||||
DESCRIPTION = "description",
|
||||
ATTACHMENT = "attachment",
|
||||
COMMENT = "comment",
|
||||
ARCHIVE = "archive",
|
||||
PHASE = "phase",
|
||||
}
|
||||
|
||||
export enum IActivityLogChangeType {
|
||||
CREATE = "create",
|
||||
UPDATE = "update",
|
||||
DELETE = "delete",
|
||||
ASSIGN = "assign",
|
||||
UNASSIGN = "unassign",
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default class EmailNotificationsService {
|
||||
public static sendStatusChange() {
|
||||
const message = "";
|
||||
}
|
||||
}
|
||||
24
worklenz-backend/src/services/notifications/interfaces.ts
Normal file
24
worklenz-backend/src/services/notifications/interfaces.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface INotification {
|
||||
user_ids: string[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface IReceiver {
|
||||
receiver_socket_id: string;
|
||||
team: string;
|
||||
team_id: string;
|
||||
message: string;
|
||||
project_id?: string;
|
||||
project?: string;
|
||||
project_color?: string;
|
||||
task_id?: string;
|
||||
}
|
||||
|
||||
export interface ICreateNotificationRequest {
|
||||
userId: string;
|
||||
teamId: string;
|
||||
socketId: string;
|
||||
message: string;
|
||||
taskId: string | null;
|
||||
projectId: string | null;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import {INotification} from "../interfaces";
|
||||
|
||||
export class TaskAdd implements INotification {
|
||||
private template = `<user> added "<task>" to the "<project>".`;
|
||||
user_ids!: string[];
|
||||
message!: string;
|
||||
|
||||
constructor(userIds: string[], userName: string, taskName: string, projectName: string) {
|
||||
this.user_ids = userIds;
|
||||
this.message = this.template
|
||||
.replace(/<user>/g, userName)
|
||||
.replace(/<task>/g, taskName)
|
||||
.replace(/<project>/g, projectName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import {INotification} from "../interfaces";
|
||||
|
||||
class TaskRemove implements INotification {
|
||||
private template = `<user> removed "<task>" from the "<project>".`;
|
||||
user_ids!: string[];
|
||||
message!: string;
|
||||
|
||||
constructor(userIds: string[], userName: string, taskName: string, projectName: string) {
|
||||
this.user_ids = userIds;
|
||||
this.message = this.template
|
||||
.replace(/<user>/g, userName)
|
||||
.replace(/<task>/g, taskName)
|
||||
.replace(/<project>/g, projectName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export const NotificationTypes = {
|
||||
NEW_TASK_ADDED: "NEW_TASK_ADDED",
|
||||
TASK_REMOVED: "TASK_REMOVED",
|
||||
ADDED_TO_A_PROJECT: "ADDED_TO_A_PROJECT",
|
||||
REMOVED_FROM_A_PROJECT: "REMOVED_FROM_A_PROJECT",
|
||||
MENTIONED_IN_A_TASK_COMMENT: "MENTIONED_IN_A_TASK_COMMENT",
|
||||
NEW_COMMENT_ADDED_TO_A_TASK: "NEW_COMMENT_ADDED_TO_A_TASK",
|
||||
TASK_TRANSITION: "TASK_TRANSITION",
|
||||
TASK_RENAME: "TASK_RENAME",
|
||||
TASK_ASSIGNMENT: "TASK_ASSIGNMENT",
|
||||
TASK_UNASSIGN: "TASK_UNASSIGN",
|
||||
ADD_TO_A_TEAM: "ADD_TO_A_TEAM",
|
||||
REMOVED_FROM_A_TEAM: "REMOVED_FROM_A_TEAM",
|
||||
};
|
||||
42
worklenz-backend/src/services/notifications/notification.ts
Normal file
42
worklenz-backend/src/services/notifications/notification.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export declare type Params = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export default class WorklenzNotification {
|
||||
private team: string;
|
||||
private team_id: string;
|
||||
private message: string;
|
||||
private url: string | null;
|
||||
private project?: string;
|
||||
private color?: string;
|
||||
private params?: Params;
|
||||
private task_id?: string;
|
||||
private project_id?: string;
|
||||
|
||||
constructor(teamName: string, teamId: string, message: string, url: string | null) {
|
||||
this.team = teamName;
|
||||
this.team_id = teamId;
|
||||
this.message = message;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public setProject(name: string) {
|
||||
this.project = name;
|
||||
}
|
||||
|
||||
public setColor(code: string) {
|
||||
this.color = code;
|
||||
}
|
||||
|
||||
public setParams(params: Params) {
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
public setTaskId(id: string) {
|
||||
this.task_id = id;
|
||||
}
|
||||
|
||||
public setProjectId(id: string) {
|
||||
this.project_id = id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import db from "../../config/db";
|
||||
import {IO} from "../../shared/io";
|
||||
import {log_error} from "../../shared/utils";
|
||||
import {SocketEvents} from "../../socket.io/events";
|
||||
import {ICreateNotificationRequest, IReceiver} from "./interfaces";
|
||||
import WorklenzNotification from "./notification";
|
||||
import {sendInvitationEmail} from "../../shared/email-templates";
|
||||
import {IPassportSession} from "../../interfaces/passport-session";
|
||||
|
||||
export class NotificationsService {
|
||||
public static TYPE_POP = 1;
|
||||
public static TYPE_EMAIL = 2;
|
||||
|
||||
private static isAllowPopup(type: number) {
|
||||
return type & this.TYPE_POP;
|
||||
}
|
||||
|
||||
private static isAllowEmail(type: number) {
|
||||
return type & this.TYPE_EMAIL;
|
||||
}
|
||||
|
||||
private static isAllowBoth(type: number) {
|
||||
return this.isAllowPopup(type) && this.isAllowEmail(type);
|
||||
}
|
||||
|
||||
public static async createTaskUpdate(type: string, reporterId: string, taskId: string, userId: string, teamId: string) {
|
||||
if (!userId || !taskId) return;
|
||||
try {
|
||||
const q = "SELECT notify_task_assignment_update($1, $2, $3, $4, $5) AS receiver;";
|
||||
const result = await db.query(q, [type, reporterId, taskId, userId, teamId]);
|
||||
const [data] = result.rows;
|
||||
const receiver = data.receiver || {};
|
||||
|
||||
if (receiver?.receiver_socket_id && (reporterId !== userId)) {
|
||||
NotificationsService.sendNotification(receiver);
|
||||
}
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
}
|
||||
}
|
||||
|
||||
public static sendNotification(receiver: IReceiver): void {
|
||||
const url = receiver.project_id ? `/worklenz/projects/${receiver.project_id}` : null;
|
||||
const notification = new WorklenzNotification(receiver.team, receiver.team_id, receiver.message, url);
|
||||
|
||||
if (receiver.project) {
|
||||
notification.setProject(receiver.project);
|
||||
}
|
||||
|
||||
if (receiver.project_color) {
|
||||
notification.setColor(receiver.project_color);
|
||||
}
|
||||
|
||||
if (receiver.task_id) {
|
||||
notification.setParams({task: receiver.task_id});
|
||||
notification.setTaskId(receiver.task_id);
|
||||
}
|
||||
|
||||
if (receiver.project_id) {
|
||||
notification.setProjectId(receiver.project_id);
|
||||
}
|
||||
|
||||
IO.emit(SocketEvents.NOTIFICATIONS_UPDATE, receiver.receiver_socket_id, notification);
|
||||
}
|
||||
|
||||
public static sendInvitation(userId: string, userName: string, teamName: string, teamId: string, teamMemberId: string) {
|
||||
const message = `<b>${userName}</b> has invited you to work with <b>${teamName}</b>.`;
|
||||
const payload = {message, team: teamName, team_id: teamId};
|
||||
IO.emitByTeamMemberId(teamMemberId, userId || null, SocketEvents.INVITATIONS_UPDATE, payload);
|
||||
}
|
||||
|
||||
public static async createNotification(request: ICreateNotificationRequest) {
|
||||
try {
|
||||
const q = "SELECT create_notification($1, $2, $3, $4, $5) AS res;";
|
||||
const result = await db.query(q, [request.userId, request.teamId, request.taskId, request.projectId, request.message]);
|
||||
const [data] = result.rows;
|
||||
const response = data.res;
|
||||
|
||||
this.sendNotification({
|
||||
receiver_socket_id: request.socketId,
|
||||
project: response.project,
|
||||
message: request.message,
|
||||
project_color: response.project_color,
|
||||
project_id: request.projectId as string,
|
||||
team: response.team,
|
||||
team_id: request.teamId
|
||||
});
|
||||
} catch (error) {
|
||||
log_error(error);
|
||||
}
|
||||
}
|
||||
|
||||
public static sendTeamMembersInvitations(members: any[], user: IPassportSession, projectId?: string) {
|
||||
for (const member of members) {
|
||||
sendInvitationEmail(
|
||||
!member.is_new,
|
||||
user,
|
||||
!member.is_new ? member.name : member.team_member_id,
|
||||
member.email,
|
||||
member.team_member_user_id,
|
||||
member.name || member.email?.split("@")[0],
|
||||
projectId
|
||||
);
|
||||
|
||||
if (member.team_member_id) {
|
||||
NotificationsService.sendInvitation(
|
||||
user.id as string,
|
||||
user.name as string,
|
||||
user.team_name as string,
|
||||
user.team_id as string,
|
||||
member.team_member_id
|
||||
);
|
||||
}
|
||||
|
||||
member.id = member.team_member_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user