This commit is contained in:
chamikaJ
2025-04-17 18:28:54 +05:30
parent f583291d8a
commit 8825b0410a
2837 changed files with 241385 additions and 127578 deletions

View File

@@ -11,13 +11,14 @@ export async function on_project_health_change(_io: Server, socket: Socket, data
const q = `UPDATE projects SET health_id = $2 WHERE id = $1;`;
await db.query(q, [body.project_id, body.health_id]);
const q2 = "SELECT color_code FROM sys_project_healths WHERE id=$1";
const q2 = "SELECT color_code, name FROM sys_project_healths WHERE id=$1";
const result = await db.query(q2, [body.health_id]);
const [d] = result.rows;
socket.emit(SocketEvents.PROJECT_HEALTH_CHANGE.toString(), {
id: body.project_id,
color_code: d.color_code,
name: d.name,
health_id: body.health_id
});
} catch (error) {

View File

@@ -8,13 +8,15 @@ import {SocketEvents} from "../events";
import {getLoggedInUserIdFromSocket, log_error, notifyProjectUpdates} from "../util";
import {logMemberAssignment} from "../../services/activity-logs/activity-logs.service";
export async function getAssignees(taskId: string): Promise<Array<{
export interface ITaskAssignee {
team_member_id?: string;
project_member_id?: string;
name?: string;
avatar_url?: string;
user_id?: string;
}>> {
}
export async function getAssignees(taskId: string): Promise<ITaskAssignee[]> {
const result1 = await db.query("SELECT get_task_assignees($1) AS assignees;", [taskId]);
const [d] = result1.rows;
const assignees = d.assignees || [];
@@ -30,7 +32,7 @@ export async function getTeamMembers(teamId: string) {
return data?.members || [];
}
async function runAssignOrRemove(data: any, isAssignment = false) {
export async function runAssignOrRemove(data: any, isAssignment = false) {
const q = isAssignment
? "SELECT create_task_assignee($1, $2, $3, $4) AS data;"
: `SELECT remove_task_assignee($1, $2, $3) AS data;`;

View File

@@ -0,0 +1,125 @@
import { Server, Socket } from "socket.io";
import { NotificationsService } from "../../services/notifications/notifications.service";
import { SocketEvents } from "../events";
import {
getLoggedInUserIdFromSocket,
log_error,
notifyProjectUpdates
} from "../util";
import { logMemberAssignment } from "../../services/activity-logs/activity-logs.service";
import { getAssignees, ITaskAssignee, runAssignOrRemove } from "./on-quick-assign-or-remove";
interface TaskAssigneesChangeData {
task_id: string;
team_id: string;
team_member_id: string[];
project_id: string;
reporter_id: string;
mode: number; // 0 for assign, 1 for unassign
}
export async function on_task_assignees_change(
_io: Server,
socket: Socket,
rawData?: string
): Promise<void> {
try {
if (!rawData) {
throw new Error("No data provided.");
}
const body: TaskAssigneesChangeData = JSON.parse(rawData);
const userId = getLoggedInUserIdFromSocket(socket);
const newAssignees: string[] = body.team_member_id;
const prevAssignees: ITaskAssignee[] = await getAssignees(body.task_id);
const removedAssignees = prevAssignees.filter(assignee => !newAssignees.includes(assignee.team_member_id || ""));
const addedAssignees = newAssignees.filter(assignee => !prevAssignees.map(a => a.team_member_id || "").includes(assignee));
if (!Array.isArray(newAssignees) || newAssignees.length === 0) {
throw new Error("Invalid or empty assignee IDs.");
}
// Handle removed assignees
const removeResults = await Promise.all(
removedAssignees.map(async (assignee) => {
const data = {
task_id: body.task_id,
team_id: body.team_id,
team_member_id: assignee.team_member_id,
project_id: body.project_id,
reporter_id: body.reporter_id
};
const assignment = await runAssignOrRemove(data, false);
// Log activity
logMemberAssignment({
task_id: body.task_id,
socket,
new_value: null,
old_value: assignee.team_member_id,
assign_type: "UNASSIGN",
});
// Notify if userId and assignment.user_id are different
if (userId && userId !== assignment.user_id) {
NotificationsService.createTaskUpdate(
"UNASSIGN",
userId,
body.task_id,
assignment.user_id,
body.team_id
);
}
return assignee.team_member_id;
})
);
// Handle new assignees
const addResults = await Promise.all(
addedAssignees.map(async (assigneeId) => {
const data = {
task_id: body.task_id,
team_id: body.team_id,
team_member_id: assigneeId,
project_id: body.project_id,
reporter_id: body.reporter_id
};
const assignment = await runAssignOrRemove(data, true);
// Log activity
logMemberAssignment({
task_id: body.task_id,
socket,
new_value: assigneeId,
old_value: null,
assign_type: "ASSIGN",
});
// Notify if userId and assignment.user_id are different
if (userId && userId !== assignment.user_id) {
NotificationsService.createTaskUpdate(
"ASSIGN",
userId,
body.task_id,
assignment.user_id,
body.team_id
);
}
return assigneeId;
})
);
// Notify project updates once after all changes
notifyProjectUpdates(socket, body.task_id);
// Emit updated assignee list
socket.emit(SocketEvents.TASK_ASSIGNEES_CHANGE.toString(), { assigneeIds: newAssignees });
} catch (error) {
log_error(error);
}
}

View File

@@ -0,0 +1,21 @@
import { Server, Socket } from "socket.io";
import { log_error } from "../util";
import db from "../../config/db";
import { SocketEvents } from "../events";
import { body } from "express-validator";
export async function on_task_billable_change(_io: Server, socket: Socket, data?: {task_id?: string, billable?: boolean}) {
if (!data?.task_id || (typeof data.billable != "boolean")) return;
try {
const q = `UPDATE tasks SET billable = $2 WHERE id = $1`;
await db.query(q, [data?.task_id, data?.billable]);
socket.emit(SocketEvents.TASK_BILLABLE_CHANGE.toString(), {
id: data?.task_id
});
} catch (e) {
log_error(e);
}
}

View File

@@ -1,21 +1,33 @@
import {Server, Socket} from "socket.io";
import { Server, Socket } from "socket.io";
import db from "../../config/db";
import {SocketEvents} from "../events";
import { SocketEvents } from "../events";
import {log_error, notifyProjectUpdates} from "../util";
import { log_error, notifyProjectUpdates } from "../util";
import sanitize from "sanitize-html";
import {getTaskDetails, logDescriptionChange} from "../../services/activity-logs/activity-logs.service";
import {
getTaskDetails,
logDescriptionChange,
} from "../../services/activity-logs/activity-logs.service";
export async function on_task_description_change(_io: Server, socket: Socket, data?: string) {
export async function on_task_description_change(
_io: Server,
socket: Socket,
data?: string
) {
try {
const body = JSON.parse(data as string);
const q = `UPDATE tasks
SET description = $2
WHERE id = $1
RETURNING description;`;
const body = JSON.parse(data as string);
const task_data = await getTaskDetails(body.task_id, "description");
const description = (body.description || "").replace(/(^([ ]*<p><br><\/p>)*)|((<p><br><\/p>)*[ ]*$)/gi, "").trim() || null;
const description =
(body.description || "")
.replace(/(^([ ]*<p><br><\/p>)*)|((<p><br><\/p>)*[ ]*$)/gi, "")
.trim() || null;
await db.query(q, [body.task_id, sanitize(description)]);
socket.emit(SocketEvents.TASK_DESCRIPTION_CHANGE.toString(), {
@@ -24,12 +36,14 @@ export async function on_task_description_change(_io: Server, socket: Socket, da
parent_task: body.parent_task,
});
logDescriptionChange({
task_id: body.task_id,
socket,
new_value: description,
old_value: task_data.description
});
if (description && task_data.description) {
logDescriptionChange({
task_id: body.task_id,
socket,
new_value: description,
old_value: task_data.description,
});
}
notifyProjectUpdates(socket, body.task_id);
// }

View File

@@ -42,8 +42,8 @@ export async function on_task_name_change(_io: Server, socket: Socket, data?: st
logNameChange({
task_id: body.task_id,
socket,
new_value: response.name,
old_value: task_data.name
new_value: response?.name,
old_value: task_data?.name
});
} catch (error) {

View File

@@ -1,6 +1,6 @@
import {Server, Socket} from "socket.io";
import db from "../../config/db";
import {PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA} from "../../shared/constants";
import {PriorityColorCodes, PriorityColorCodesDark, TASK_PRIORITY_COLOR_ALPHA} from "../../shared/constants";
import {SocketEvents} from "../events";
import {log_error, notifyProjectUpdates} from "../util";
@@ -19,11 +19,13 @@ export async function on_task_priority_change(_io: Server, socket: Socket, data?
const [d] = result.rows;
d.color_code = (PriorityColorCodes[d.value] || PriorityColorCodes["0"]) + TASK_PRIORITY_COLOR_ALPHA;
d.color_code_dark = PriorityColorCodesDark[d.value] || PriorityColorCodesDark["0"];
socket.emit(SocketEvents.TASK_PRIORITY_CHANGE.toString(), {
id: body.task_id,
parent_task: body.parent_task,
color_code: d.color_code,
color_code_dark: d.color_code_dark,
priority_id: body.priority_id
});

View File

@@ -0,0 +1,29 @@
import { Server, Socket } from "socket.io";
import { log_error } from "../util";
import { SocketEvents } from "../events";
import TasksRecurringController from "../../controllers/task-recurring-controller";
export async function on_task_recurring_change(_io: Server, socket: Socket, data?: { task_id?: string, schedule_id?: string }) {
if (!data?.task_id) return;
try {
if (!data.schedule_id) {
const scheduleData = await TasksRecurringController.createTaskSchedule(data.task_id);
socket.emit(SocketEvents.TASK_RECURRING_CHANGE.toString(), {
task_id: data?.task_id,
id: scheduleData.id,
schedule_type: scheduleData.schedule_type
});
} else {
await TasksRecurringController.removeTaskSchedule(data.schedule_id);
socket.emit(SocketEvents.TASK_RECURRING_CHANGE.toString(), {
task_id: data?.task_id,
id: null,
schedule_type: null
});
}
} catch (e) {
log_error(e);
}
}

View File

@@ -46,7 +46,7 @@ function notifyStatusChange(socket: Socket, config: Config) {
async function emitSortOrderChange(data: ChangeRequest, socket: Socket) {
const q = `
SELECT id, sort_order
SELECT id, sort_order, completed_at
FROM tasks
WHERE project_id = $1
ORDER BY sort_order;
@@ -77,7 +77,14 @@ export async function on_task_sort_order_change(_io: Server, socket: Socket, dat
to_last_index: Boolean(data.to_last_index)
};
if (config.group_by === GroupBy.STATUS) {
if ((config.group_by === GroupBy.STATUS) && config.to_group) {
const canContinue = await TasksControllerV2.checkForCompletedDependencies(config.task_id, config?.to_group);
if (!canContinue) {
return socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
completed_deps: canContinue
});
}
notifyStatusChange(socket, config);
}

View File

@@ -13,8 +13,22 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?:
try {
const body = JSON.parse(data as string);
const userId = getLoggedInUserIdFromSocket(socket);
const task_data = await getTaskDetails(body.task_id, "status_id");
const taskData = await getTaskDetails(body.task_id, "status_id");
const canContinue = await TasksControllerV2.checkForCompletedDependencies(body.task_id, body.status_id);
if (!canContinue) {
const {color_code, color_code_dark} = await TasksControllerV2.getTaskStatusColor(taskData.status_id);
return socket.emit(SocketEvents.TASK_STATUS_CHANGE.toString(), {
id: body.task_id,
parent_task: body.parent_task,
status_id: taskData.status_id,
color_code: color_code + TASK_STATUS_COLOR_ALPHA,
color_code_dark,
completed_deps: canContinue
});
}
const q2 = "SELECT handle_on_task_status_change($1, $2, $3) AS res;";
const results1 = await db.query(q2, [userId, body.task_id, body.status_id]);
const [d] = results1.rows;
@@ -41,12 +55,14 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?:
id: body.task_id,
parent_task: body.parent_task,
color_code: changeResponse.color_code,
color_code_dark: changeResponse.color_code_dark,
complete_ratio: info?.ratio,
completed_count: info?.total_completed,
total_tasks_count: info?.total_tasks,
status_id: body.status_id,
completed_at: changeResponse.completed_at,
statusCategory: changeResponse.status_category
statusCategory: changeResponse.status_category,
completed_deps: canContinue
});
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), {
@@ -67,7 +83,7 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?:
task_id: body.task_id,
socket,
new_value: body.status_id,
old_value: task_data.status_id
old_value: taskData.status_id
});
notifyProjectUpdates(socket, body.task_id);

View File

@@ -0,0 +1,64 @@
import { Server, Socket } from "socket.io";
import { log_error } from "../util";
import db from "../../config/db";
import { SocketEvents } from "../events";
interface CustomColumnPinnedChangeData {
column_id: string;
project_id: string;
is_visible: boolean;
}
export const on_custom_column_pinned_change = async (io: Server, socket: Socket, data: string) => {
try {
// Parse the data
const parsedData: CustomColumnPinnedChangeData = typeof data === "string" ? JSON.parse(data) : data;
const { column_id, project_id, is_visible } = parsedData;
// Validate input data
if (!column_id || !project_id || is_visible === undefined) {
log_error("Invalid data for custom column pinned change");
return;
}
// Update the is_visible status in the database
const updateQuery = `
UPDATE cc_custom_columns
SET is_visible = $1,
updated_at = NOW()
WHERE id = $2 AND project_id = $3
RETURNING id, key
`;
const result = await db.query(updateQuery, [is_visible, column_id, project_id]);
if (result.rowCount === 0) {
log_error("Custom column not found or not updated");
return;
}
const updatedColumn = result.rows[0];
// Broadcast the update to all clients in the project room
socket.to(`project:${project_id}`).emit(
SocketEvents.CUSTOM_COLUMN_PINNED_CHANGE.toString(),
JSON.stringify({
column_id,
column_key: updatedColumn.key,
is_visible
})
);
// Also send back to the sender for confirmation
socket.emit(
SocketEvents.CUSTOM_COLUMN_PINNED_CHANGE.toString(),
JSON.stringify({
column_id,
column_key: updatedColumn.key,
is_visible
})
);
} catch (error) {
log_error(error);
}
};

View File

@@ -0,0 +1,137 @@
import { Server, Socket } from "socket.io";
import { SocketEvents } from "../events";
import db from "../../config/db";
import { log_error } from "../util";
interface TaskCustomColumnUpdateData {
task_id: string;
column_key: string;
value: string | number | boolean;
project_id: string;
}
export const on_task_custom_column_update = async (_io: Server, socket: Socket, data: string) => {
try {
// Parse the data
const parsedData: TaskCustomColumnUpdateData = typeof data === "string" ? JSON.parse(data) : data;
const { task_id, column_key, value, project_id } = parsedData;
if (!task_id || !column_key || value === undefined || !project_id) {
console.error("Invalid data for task custom column update", { task_id, column_key, value, project_id });
return;
}
// Get column information
const columnQuery = `
SELECT id, field_type
FROM cc_custom_columns
WHERE project_id = $1 AND key = $2
`;
const columnResult = await db.query(columnQuery, [project_id, column_key]);
if (columnResult.rowCount === 0) {
console.error("Custom column not found", { project_id, column_key });
return;
}
const column = columnResult.rows[0];
const columnId = column.id;
const fieldType = column.field_type;
// Determine which value field to use based on the field_type
let textValue = null;
let numberValue = null;
let dateValue = null;
let booleanValue = null;
let jsonValue = null;
switch (fieldType) {
case "number":
numberValue = parseFloat(String(value));
break;
case "date":
dateValue = new Date(String(value));
break;
case "checkbox":
booleanValue = Boolean(value);
break;
case "people":
jsonValue = JSON.stringify(Array.isArray(value) ? value : [value]);
break;
default:
textValue = String(value);
}
// Check if a value already exists
const existingValueQuery = `
SELECT id
FROM cc_column_values
WHERE task_id = $1 AND column_id = $2
`;
const existingValueResult = await db.query(existingValueQuery, [task_id, columnId]);
if (existingValueResult.rowCount && existingValueResult.rowCount > 0) {
// Update existing value
const updateQuery = `
UPDATE cc_column_values
SET text_value = $1,
number_value = $2,
date_value = $3,
boolean_value = $4,
json_value = $5,
updated_at = NOW()
WHERE task_id = $6 AND column_id = $7
`;
await db.query(updateQuery, [
textValue,
numberValue,
dateValue,
booleanValue,
jsonValue,
task_id,
columnId
]);
} else {
// Insert new value
const insertQuery = `
INSERT INTO cc_column_values
(task_id, column_id, text_value, number_value, date_value, boolean_value, json_value, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
`;
await db.query(insertQuery, [
task_id,
columnId,
textValue,
numberValue,
dateValue,
booleanValue,
jsonValue
]);
}
// Broadcast the update to all clients in the project room
socket.to(`project:${project_id}`).emit(
SocketEvents.TASK_CUSTOM_COLUMN_UPDATE.toString(),
JSON.stringify({
task_id,
column_key,
value
})
);
// Also send back to the sender for confirmation
socket.emit(
SocketEvents.TASK_CUSTOM_COLUMN_UPDATE.toString(),
JSON.stringify({
task_id,
column_key,
value
})
);
console.log("Task custom column updated successfully", { task_id, column_key });
} catch (error) {
log_error(error);
console.error("Error updating task custom column", error);
}
};

View File

@@ -51,5 +51,10 @@ export enum SocketEvents {
SCHEDULE_MEMBER_ALLOCATION_CREATE,
SCHEDULE_MEMBER_START_DATE_CHANGE,
SCHEDULE_MEMBER_END_DATE_CHANGE,
PROJECT_DATA_CHANGE
PROJECT_DATA_CHANGE,
TASK_BILLABLE_CHANGE,
TASK_RECURRING_CHANGE,
TASK_ASSIGNEES_CHANGE,
TASK_CUSTOM_COLUMN_UPDATE,
CUSTOM_COLUMN_PINNED_CHANGE,
}

View File

@@ -47,6 +47,11 @@ import { on_gannt_drag_change } from "./commands/on_gannt_drag_change";
import { on_schedule_member_start_date_change } from "./commands/on_schedule_member_start_date_change";
import { on_schedule_member_end_date_change } from "./commands/on_schedule_member_end_date_change";
import { on_schedule_member_allocation_create } from "./commands/on_schedule_member_allocation_create";
import { on_task_billable_change } from "./commands/on-task-billable-change";
import { on_task_recurring_change } from "./commands/on-task-recurring-change";
import { on_task_assignees_change } from "./commands/on-task-assignees-change";
import { on_task_custom_column_update } from "./commands/on_custom_column_update";
import { on_custom_column_pinned_change } from "./commands/on_custom_column_pinned_change";
export function register(io: any, socket: Socket) {
log(socket.id, "client registered");
@@ -96,7 +101,12 @@ export function register(io: any, socket: Socket) {
socket.on(SocketEvents.SCHEDULE_MEMBER_ALLOCATION_CREATE.toString(), data => on_schedule_member_allocation_create(io, socket, data));
socket.on(SocketEvents.SCHEDULE_MEMBER_START_DATE_CHANGE.toString(), data => on_schedule_member_start_date_change(io, socket, data));
socket.on(SocketEvents.SCHEDULE_MEMBER_END_DATE_CHANGE.toString(), data => on_schedule_member_end_date_change(io, socket, data));
socket.on(SocketEvents.TASK_BILLABLE_CHANGE.toString(), data => on_task_billable_change(io, socket, data));
socket.on(SocketEvents.TASK_RECURRING_CHANGE.toString(), data => on_task_recurring_change(io, socket, data));
socket.on(SocketEvents.TASK_ASSIGNEES_CHANGE.toString(), data => on_task_assignees_change(io, socket, data));
socket.on(SocketEvents.TASK_CUSTOM_COLUMN_UPDATE.toString(), data => on_task_custom_column_update(io, socket, data));
socket.on(SocketEvents.CUSTOM_COLUMN_PINNED_CHANGE.toString(), data => on_custom_column_pinned_change(io, socket, data));
// socket.io built-in event
socket.on("disconnect", (reason) => on_disconnect(io, socket, reason));
}