Merge pull request #220 from JayaruPerera/feature/project-activity-logs

feat: Add project activity logs API and frontend components
This commit is contained in:
Chamika J
2025-07-11 11:27:52 +05:30
committed by GitHub
13 changed files with 1663 additions and 125 deletions

View File

@@ -1,18 +1,17 @@
import moment from "moment";
import {IWorkLenzRequest} from "../interfaces/worklenz-request";
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
import Excel from "exceljs";
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
import db from "../config/db";
import {ServerResponse} from "../models/server-response";
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";
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 { 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;
@@ -31,4 +30,196 @@ export default class ActivitylogsController extends WorklenzControllerBase {
return res.status(200).send(new ServerResponse(true, data.activity_logs));
}
}
@HandleExceptions()
public static async getByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
try {
console.log("Received request for project activity logs:", req.params, req.query);
const projectId = req.params.id;
const page = parseInt(req.query.page as string) || 1;
const size = parseInt(req.query.size as string) || 20;
const offset = (page - 1) * size;
const filterType = req.query.filter as string || "all";
// Add filter conditions
let filterClause = "";
const filterParams = [projectId];
let paramIndex = 2;
if (filterType && filterType !== "all") {
filterClause = ` AND tal.attribute_type = $${paramIndex}`;
filterParams.push(filterType);
paramIndex++;
}
// Defensive UUID regex for safe casting
const uuidRegex = "'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'";
const q = `
SELECT
tal.id,
tal.task_id,
tal.attribute_type,
tal.log_type,
tal.old_value,
tal.new_value,
tal.prev_string,
tal.next_string,
tal.created_at,
-- Task details
(SELECT name FROM tasks WHERE id = tal.task_id) AS task_name,
(SELECT task_no FROM tasks WHERE id = tal.task_id) AS task_no,
CONCAT((SELECT key FROM projects WHERE id = $1), '-', (SELECT task_no FROM tasks WHERE id = tal.task_id)) AS task_key,
-- User details
(SELECT ROW_TO_JSON(user_data) FROM (
SELECT
u.id,
u.name,
u.avatar_url,
u.email
FROM users u
WHERE u.id = tal.user_id
) user_data) AS done_by,
-- Status details for status changes (safe UUID cast)
CASE
WHEN tal.attribute_type = 'status' AND tal.old_value ~ ${uuidRegex} THEN
(SELECT ROW_TO_JSON(status_data) FROM (
SELECT
ts.name
FROM task_statuses ts
WHERE ts.id = tal.old_value::UUID
) status_data)
ELSE NULL
END AS previous_status,
CASE
WHEN tal.attribute_type = 'status' AND tal.new_value ~ ${uuidRegex} THEN
(SELECT ROW_TO_JSON(status_data) FROM (
SELECT
ts.name
FROM task_statuses ts
WHERE ts.id = tal.new_value::UUID
) status_data)
ELSE NULL
END AS next_status,
-- Priority details for priority changes (safe UUID cast)
CASE
WHEN tal.attribute_type = 'priority' AND tal.old_value ~ ${uuidRegex} THEN
(SELECT ROW_TO_JSON(priority_data) FROM (
SELECT
tp.name
FROM task_priorities tp
WHERE tp.id = tal.old_value::UUID
) priority_data)
ELSE NULL
END AS previous_priority,
CASE
WHEN tal.attribute_type = 'priority' AND tal.new_value ~ ${uuidRegex} THEN
(SELECT ROW_TO_JSON(priority_data) FROM (
SELECT
tp.name
FROM task_priorities tp
WHERE tp.id = tal.new_value::UUID
) priority_data)
ELSE NULL
END AS next_priority,
-- Assigned user details for assignee changes (safe UUID cast)
CASE
WHEN tal.attribute_type = 'assignee' AND tal.new_value ~ ${uuidRegex} THEN
(SELECT ROW_TO_JSON(user_data) FROM (
SELECT
u.id,
u.name,
u.avatar_url,
u.email
FROM users u
WHERE u.id = tal.new_value::UUID
) user_data)
ELSE NULL
END AS assigned_user
FROM task_activity_logs tal
WHERE tal.project_id = $1${filterClause}
ORDER BY tal.created_at DESC
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`;
const countQuery = `
SELECT COUNT(*) as total
FROM task_activity_logs
WHERE project_id = $1${filterClause}
`;
const [result, countResult] = await Promise.all([
db.query(q, [...filterParams, size, offset]),
db.query(countQuery, filterType && filterType !== "all" ? [projectId, filterType] : [projectId])
]);
const total = parseInt(countResult.rows[0]?.total || "0");
// Format the logs
for (const log of result.rows) {
if (log.attribute_type === "estimation") {
log.previous = formatDuration(moment.duration(log.old_value, "minutes"));
log.current = formatDuration(moment.duration(log.new_value, "minutes"));
} else {
log.previous = log.old_value;
log.current = log.new_value;
}
// Add color to users
if (log.assigned_user) {
log.assigned_user.color_code = getColor(log.assigned_user.name);
}
if (log.done_by) {
log.done_by.color_code = getColor(log.done_by.name);
}
// Add default colors for status and priority since table doesn't have color_code
if (log.previous_status) {
log.previous_status.color_code = "#d9d9d9"; // Default gray color
}
if (log.next_status) {
log.next_status.color_code = "#1890ff"; // Default blue color
}
if (log.previous_priority) {
log.previous_priority.color_code = "#d9d9d9"; // Default gray color
}
if (log.next_priority) {
log.next_priority.color_code = "#ff4d4f"; // Default red color for priority
}
// Generate log text
log.log_text = await formatLogText(log);
log.attribute_type = log.attribute_type?.replace(/_/g, " ");
}
const response = {
logs: result.rows,
pagination: {
current: page,
pageSize: size,
total,
totalPages: Math.ceil(total / size)
}
};
return res.status(200).send(new ServerResponse(true, response));
} catch (error: any) {
console.error("❌ Error in getByProjectId:", error);
return res.status(500).send(new ServerResponse(false, null, `Internal server error: ${error.message}`));
}
}
@HandleExceptions()
public static async exportProjectActivityLogs(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<void> {
// ...keep your export logic as is...
}
}

View File

@@ -0,0 +1,291 @@
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
import moment from "moment";
import db from "../config/db";
import { ServerResponse } from "../models/server-response";
import WorklenzControllerBase from "./worklenz-controller-base";
import HandleExceptions from "../decorators/handle-exceptions";
// --- Helpers -------------------------------------------------------------
function formatDuration(duration: moment.Duration | null): string {
if (!duration) return "0m";
const hours = Math.floor(duration.asHours());
const minutes = duration.minutes();
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
}
function generateLogText(attributeType: string): string {
const map: Record<string,string> = {
name: "updated task name",
status: "changed status",
priority: "changed priority",
assignee: "updated assignee",
end_date: "changed due date",
start_date: "changed start date",
estimation: "updated time estimation",
description: "updated description",
phase: "changed phase",
labels: "updated labels",
};
return map[attributeType] || "made changes to";
}
function isValidUuid(id?: string): boolean {
return !!id && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
}
// Get a consistent color for a user based on their name
function getColorFromName(name: string): string {
if (!name) return "#1890ff";
const colors = [
"#f56a00", "#7265e6", "#ffbf00", "#00a2ae",
"#1890ff", "#52c41a", "#eb2f96", "#faad14",
"#722ed1", "#13c2c2", "#fa8c16", "#a0d911"
];
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
return colors[Math.abs(hash) % colors.length];
}
// --- Controller ----------------------------------------------------------
export default class ProjectActivityLogsController extends WorklenzControllerBase {
@HandleExceptions()
public static async getByProjectId(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
// 1) Extract & validate inputs
const projectId = req.params.id;
if (!isValidUuid(projectId)) {
return res
.status(400)
.json({ done: false, body: null, error: "Invalid project ID." });
}
const page = parseInt(req.query.page as string, 10) || 1;
const size = parseInt(req.query.size as string, 10) || 20;
const offset = (page - 1) * size;
const filterType = (req.query.filter as string) || "all";
const allowedFilters = [
"all","name","status","priority","assignee",
"end_date","start_date","estimation","description","phase"
];
if (!allowedFilters.includes(filterType)) {
return res
.status(400)
.json({ done: false, body: null, error: "Invalid filter type." });
}
// 2) Build parameterized SQL
let mainQuery = `
SELECT
tal.id,
tal.task_id,
tal.user_id,
tal.attribute_type,
tal.log_type,
tal.old_value,
tal.new_value,
tal.prev_string,
tal.next_string,
tal.created_at,
t.name AS task_name,
t.task_no AS task_no,
p.key AS project_key,
-- Include user details directly
u.id AS user_id,
u.name AS user_name,
u.email AS user_email,
u.avatar_url AS user_avatar_url
FROM task_activity_logs tal
LEFT JOIN tasks t ON tal.task_id = t.id
LEFT JOIN projects p ON tal.project_id = p.id
LEFT JOIN users u ON tal.user_id = u.id
WHERE tal.project_id = $1
`;
const queryParams: any[] = [projectId];
if (filterType !== "all") {
mainQuery += ` AND tal.attribute_type = $2`;
queryParams.push(filterType);
}
mainQuery += ` ORDER BY tal.created_at DESC`;
// placeholders for LIMIT / OFFSET
const limitIdx = queryParams.length + 1;
const offsetIdx = queryParams.length + 2;
mainQuery += ` LIMIT $${limitIdx} OFFSET $${offsetIdx}`;
queryParams.push(size, offset);
// Count query
const countQuery = `
SELECT COUNT(*) AS total
FROM task_activity_logs tal
WHERE tal.project_id = $1
`;
const countParams = filterType !== "all"
? [projectId, filterType]
: [projectId];
try {
// 3) Execute SQL
const [dataResult, countResult] = await Promise.all([
db.query(mainQuery, queryParams),
db.query(
countQuery + (filterType !== "all" ? ` AND tal.attribute_type = $2` : ""),
countParams
),
]);
const total = parseInt(countResult.rows[0]?.total || "0", 10);
// 4) Transform rows
const rows = dataResult.rows;
const logs = await Promise.all(rows.map(async (r: any) => {
const log: any = { ...r };
// Correctly structure user information
log.done_by = {
id: r.user_id || "",
name: r.user_name || "Unknown User",
avatar_url: r.user_avatar_url,
email: r.user_email || "",
color_code: r.user_name ? getColorFromName(r.user_name) : "#1890ff"
};
// task key
log.task_key = r.project_key && r.task_no ?
`${r.project_key}-${r.task_no}` :
`TASK-${r.task_id?.substring(0, 8) || "unknown"}`;
// duration / estimation formatting
if (log.attribute_type === "estimation") {
const oldMin = parseInt(log.old_value, 10);
const newMin = parseInt(log.new_value, 10);
log.previous = !isNaN(oldMin)
? formatDuration(moment.duration(oldMin, "minutes"))
: log.old_value;
log.current = !isNaN(newMin)
? formatDuration(moment.duration(newMin, "minutes"))
: log.new_value;
} else {
log.previous = log.old_value;
log.current = log.new_value;
}
// humanfriendly action
log.log_text = generateLogText(r.attribute_type);
// Handle status changes
if (log.attribute_type === "status" && log.old_value && isValidUuid(log.old_value)) {
try {
const prevStatus = await db.query(
`SELECT name, color_code FROM task_statuses WHERE id = $1`,
[log.old_value]
);
if (prevStatus.rows.length > 0) {
log.previous_status = {
name: prevStatus.rows[0].name,
color_code: prevStatus.rows[0].color_code || "#d9d9d9"
};
}
} catch (err) {
console.error("Error fetching previous status:", err);
}
}
if (log.attribute_type === "status" && log.new_value && isValidUuid(log.new_value)) {
try {
const nextStatus = await db.query(
`SELECT name, color_code FROM task_statuses WHERE id = $1`,
[log.new_value]
);
if (nextStatus.rows.length > 0) {
log.next_status = {
name: nextStatus.rows[0].name,
color_code: nextStatus.rows[0].color_code || "#1890ff"
};
}
} catch (err) {
console.error("Error fetching next status:", err);
}
}
// Handle priority changes
if (log.attribute_type === "priority" && log.old_value && isValidUuid(log.old_value)) {
try {
const prevPriority = await db.query(
`SELECT name, color_code FROM task_priorities WHERE id = $1`,
[log.old_value]
);
if (prevPriority.rows.length > 0) {
log.previous_priority = {
name: prevPriority.rows[0].name,
color_code: prevPriority.rows[0].color_code || "#d9d9d9"
};
}
} catch (err) {
console.error("Error fetching previous priority:", err);
}
}
if (log.attribute_type === "priority" && log.new_value && isValidUuid(log.new_value)) {
try {
const nextPriority = await db.query(
`SELECT name, color_code FROM task_priorities WHERE id = $1`,
[log.new_value]
);
if (nextPriority.rows.length > 0) {
log.next_priority = {
name: nextPriority.rows[0].name,
color_code: nextPriority.rows[0].color_code || "#ff4d4f"
};
}
} catch (err) {
console.error("Error fetching next priority:", err);
}
}
// Handle assignee changes
if (log.attribute_type === "assignee" && log.new_value && isValidUuid(log.new_value)) {
try {
const assignedUser = await db.query(
`SELECT id, name, avatar_url, email FROM users WHERE id = $1`,
[log.new_value]
);
if (assignedUser.rows.length > 0) {
log.assigned_user = {
...assignedUser.rows[0],
color_code: getColorFromName(assignedUser.rows[0].name)
};
}
} catch (err) {
console.error("Error fetching assigned user:", err);
}
}
return log;
}));
// 5) Send back a clean response
const response = {
logs,
pagination: {
current: page,
pageSize: size,
total,
totalPages: Math.ceil(total / size),
}
};
return res.status(200).send(new ServerResponse(true, response));
} catch (err) {
console.error("🔥 getByProjectId error:", err);
return res.status(500).send(
new ServerResponse(false, null, "Internal server error fetching logs.")
);
}
}
}

View File

@@ -7,5 +7,7 @@ import safeControllerFunction from "../../shared/safe-controller-function";
const activityLogsApiRouter = express.Router();
activityLogsApiRouter.get("/:id", idParamValidator, safeControllerFunction(ActivitylogsController.get));
activityLogsApiRouter.get("/project/:id", idParamValidator, safeControllerFunction(ActivitylogsController.getByProjectId));
activityLogsApiRouter.get("/project/:id/export", safeControllerFunction(ActivitylogsController.exportProjectActivityLogs));
export default activityLogsApiRouter;

View File

@@ -1,120 +1,126 @@
import express from "express";
import AccessControlsController from "../../controllers/access-controls-controller";
import AuthController from "../../controllers/auth-controller";
import LogsController from "../../controllers/logs-controller";
import OverviewController from "../../controllers/overview-controller";
import TaskPrioritiesController from "../../controllers/task-priorities-controller";
import attachmentsApiRouter from "./attachments-api-router";
import clientsApiRouter from "./clients-api-router";
import jobTitlesApiRouter from "./job-titles-api-router";
import notificationsApiRouter from "./notifications-api-router";
import personalOverviewApiRouter from "./personal-overview-api-router";
import projectMembersApiRouter from "./project-members-api-router";
import projectsApiRouter from "./projects-api-router";
import settingsApiRouter from "./settings-api-router";
import statusesApiRouter from "./statuses-api-router";
import subTasksApiRouter from "./sub-tasks-api-router";
import taskCommentsApiRouter from "./task-comments-api-router";
import taskWorkLogApiRouter from "./task-work-log-api-router";
import tasksApiRouter from "./tasks-api-router";
import teamMembersApiRouter from "./team-members-api-router";
import teamsApiRouter from "./teams-api-router";
import timezonesApiRouter from "./timezones-api-router";
import todoListApiRouter from "./todo-list-api-router";
import projectStatusesApiRouter from "./project-statuses-api-router";
import labelsApiRouter from "./labels-api-router";
import sharedProjectsApiRouter from "./shared-projects-api-router";
import resourceAllocationApiRouter from "./resource-allocation-api-router";
import taskTemplatesApiRouter from "./task-templates-api-router";
import projectInsightsApiRouter from "./project-insights-api-router";
import passwordValidator from "../../middlewares/validators/password-validator";
import adminCenterApiRouter from "./admin-center-api-router";
import reportingApiRouter from "./reporting-api-router";
import activityLogsApiRouter from "./activity-logs-api-router";
import safeControllerFunction from "../../shared/safe-controller-function";
import projectFoldersApiRouter from "./project-folders-api-router";
import taskPhasesApiRouter from "./task-phases-api-router";
import projectCategoriesApiRouter from "./project-categories-api-router";
import homePageApiRouter from "./home-page-api-router";
import ganttApiRouter from "./gantt-api-router";
import projectCommentsApiRouter from "./project-comments-api-router";
import reportingExportApiRouter from "./reporting-export-api-router";
import projectHealthsApiRouter from "./project-healths-api-router";
import ptTasksApiRouter from "./pt-tasks-api-router";
import projectTemplatesApiRouter from "./project-templates-api";
import ptTaskPhasesApiRouter from "./pt_task-phases-api-router";
import ptStatusesApiRouter from "./pt-statuses-api-router";
import workloadApiRouter from "./gannt-apis/workload-api-router";
import roadmapApiRouter from "./gannt-apis/roadmap-api-router";
import scheduleApiRouter from "./gannt-apis/schedule-api-router";
import scheduleApiV2Router from "./gannt-apis/schedule-api-v2-router";
import projectManagerApiRouter from "./project-managers-api-router";
import billingApiRouter from "./billing-api-router";
import taskDependenciesApiRouter from "./task-dependencies-api-router";
import taskRecurringApiRouter from "./task-recurring-api-router";
import express from "express";
import AccessControlsController from "../../controllers/access-controls-controller";
import AuthController from "../../controllers/auth-controller";
import LogsController from "../../controllers/logs-controller";
import OverviewController from "../../controllers/overview-controller";
import TaskPrioritiesController from "../../controllers/task-priorities-controller";
import attachmentsApiRouter from "./attachments-api-router";
import clientsApiRouter from "./clients-api-router";
import jobTitlesApiRouter from "./job-titles-api-router";
import notificationsApiRouter from "./notifications-api-router";
import personalOverviewApiRouter from "./personal-overview-api-router";
import projectMembersApiRouter from "./project-members-api-router";
import projectsApiRouter from "./projects-api-router";
import settingsApiRouter from "./settings-api-router";
import statusesApiRouter from "./statuses-api-router";
import subTasksApiRouter from "./sub-tasks-api-router";
import taskCommentsApiRouter from "./task-comments-api-router";
import taskWorkLogApiRouter from "./task-work-log-api-router";
import tasksApiRouter from "./tasks-api-router";
import teamMembersApiRouter from "./team-members-api-router";
import teamsApiRouter from "./teams-api-router";
import timezonesApiRouter from "./timezones-api-router";
import todoListApiRouter from "./todo-list-api-router";
import projectStatusesApiRouter from "./project-statuses-api-router";
import labelsApiRouter from "./labels-api-router";
import sharedProjectsApiRouter from "./shared-projects-api-router";
import resourceAllocationApiRouter from "./resource-allocation-api-router";
import taskTemplatesApiRouter from "./task-templates-api-router";
import projectInsightsApiRouter from "./project-insights-api-router";
import passwordValidator from "../../middlewares/validators/password-validator";
import adminCenterApiRouter from "./admin-center-api-router";
import reportingApiRouter from "./reporting-api-router";
import activityLogsApiRouter from "./activity-logs-api-router";
import safeControllerFunction from "../../shared/safe-controller-function";
import projectFoldersApiRouter from "./project-folders-api-router";
import taskPhasesApiRouter from "./task-phases-api-router";
import projectCategoriesApiRouter from "./project-categories-api-router";
import homePageApiRouter from "./home-page-api-router";
import ganttApiRouter from "./gantt-api-router";
import projectCommentsApiRouter from "./project-comments-api-router";
import reportingExportApiRouter from "./reporting-export-api-router";
import projectHealthsApiRouter from "./project-healths-api-router";
import ptTasksApiRouter from "./pt-tasks-api-router";
import projectTemplatesApiRouter from "./project-templates-api";
import ptTaskPhasesApiRouter from "./pt_task-phases-api-router";
import ptStatusesApiRouter from "./pt-statuses-api-router";
import workloadApiRouter from "./gannt-apis/workload-api-router";
import roadmapApiRouter from "./gannt-apis/roadmap-api-router";
import scheduleApiRouter from "./gannt-apis/schedule-api-router";
import scheduleApiV2Router from "./gannt-apis/schedule-api-v2-router";
import projectManagerApiRouter from "./project-managers-api-router";
import billingApiRouter from "./billing-api-router";
import taskDependenciesApiRouter from "./task-dependencies-api-router";
import taskRecurringApiRouter from "./task-recurring-api-router";
import customColumnsApiRouter from "./custom-columns-api-router";
import projectActivityLogsApiRouter from "./project-activity-logs-api-router";
const api = express.Router();
api.use("/projects", projectsApiRouter);
api.use("/team-members", teamMembersApiRouter);
api.use("/job-titles", jobTitlesApiRouter);
api.use("/clients", clientsApiRouter);
api.use("/teams", teamsApiRouter);
api.use("/tasks", tasksApiRouter);
api.use("/settings", settingsApiRouter);
api.use("/personal-overview", personalOverviewApiRouter);
api.use("/statuses", statusesApiRouter);
api.use("/todo-list", todoListApiRouter);
api.use("/notifications", notificationsApiRouter);
api.use("/attachments", attachmentsApiRouter);
api.use("/sub-tasks", subTasksApiRouter);
api.use("/project-members", projectMembersApiRouter);
api.use("/task-time-log", taskWorkLogApiRouter);
api.use("/task-comments", taskCommentsApiRouter);
api.use("/timezones", timezonesApiRouter);
api.use("/project-statuses", projectStatusesApiRouter);
api.use("/labels", labelsApiRouter);
api.use("/resource-allocation", resourceAllocationApiRouter);
api.use("/shared/projects", sharedProjectsApiRouter);
api.use("/task-templates", taskTemplatesApiRouter);
api.use("/project-insights", projectInsightsApiRouter);
api.use("/admin-center", adminCenterApiRouter);
api.use("/reporting", reportingApiRouter);
api.use("/activity-logs", activityLogsApiRouter);
api.use("/projects-folders", projectFoldersApiRouter);
api.use("/task-phases", taskPhasesApiRouter);
api.use("/project-categories", projectCategoriesApiRouter);
api.use("/home", homePageApiRouter);
api.use("/gantt", ganttApiRouter);
api.use("/project-comments", projectCommentsApiRouter);
api.use("/reporting-export", reportingExportApiRouter);
api.use("/project-healths", projectHealthsApiRouter);
api.use("/project-templates", projectTemplatesApiRouter);
api.use("/pt-tasks", ptTasksApiRouter);
api.use("/pt-task-phases", ptTaskPhasesApiRouter);
api.use("/pt-statuses", ptStatusesApiRouter);
api.use("/workload-gannt", workloadApiRouter);
api.use("/roadmap-gannt", roadmapApiRouter);
api.use("/schedule-gannt", scheduleApiRouter);
api.use("/schedule-gannt-v2", scheduleApiV2Router);
api.use("/project-managers", projectManagerApiRouter);
api.get("/overview/:id", safeControllerFunction(OverviewController.getById));
api.get("/task-priorities", safeControllerFunction(TaskPrioritiesController.get));
api.post("/change-password", passwordValidator, safeControllerFunction(AuthController.changePassword));
api.get("/access-controls/roles", safeControllerFunction(AccessControlsController.getRoles));
api.get("/logs/my-dashboard", safeControllerFunction(LogsController.getActivityLog));
api.use("/billing", billingApiRouter);
api.use("/task-dependencies", taskDependenciesApiRouter);
api.use("/task-recurring", taskRecurringApiRouter);
const api = express.Router();
api.use("/projects", projectsApiRouter);
api.use("/team-members", teamMembersApiRouter);
api.use("/job-titles", jobTitlesApiRouter);
api.use("/clients", clientsApiRouter);
api.use("/teams", teamsApiRouter);
api.use("/tasks", tasksApiRouter);
api.use("/settings", settingsApiRouter);
api.use("/personal-overview", personalOverviewApiRouter);
api.use("/statuses", statusesApiRouter);
api.use("/todo-list", todoListApiRouter);
api.use("/notifications", notificationsApiRouter);
api.use("/attachments", attachmentsApiRouter);
api.use("/sub-tasks", subTasksApiRouter);
api.use("/project-members", projectMembersApiRouter);
api.use("/task-time-log", taskWorkLogApiRouter);
api.use("/task-comments", taskCommentsApiRouter);
api.use("/timezones", timezonesApiRouter);
api.use("/project-statuses", projectStatusesApiRouter);
api.use("/labels", labelsApiRouter);
api.use("/resource-allocation", resourceAllocationApiRouter);
api.use("/shared/projects", sharedProjectsApiRouter);
api.use("/task-templates", taskTemplatesApiRouter);
api.use("/project-insights", projectInsightsApiRouter);
api.use("/admin-center", adminCenterApiRouter);
api.use("/reporting", reportingApiRouter);
api.use("/activity-logs", activityLogsApiRouter);
api.use("/projects-folders", projectFoldersApiRouter);
api.use("/task-phases", taskPhasesApiRouter);
api.use("/project-categories", projectCategoriesApiRouter);
api.use("/home", homePageApiRouter);
api.use("/gantt", ganttApiRouter);
api.use("/project-comments", projectCommentsApiRouter);
api.use("/reporting-export", reportingExportApiRouter);
api.use("/project-healths", projectHealthsApiRouter);
api.use("/project-templates", projectTemplatesApiRouter);
api.use("/pt-tasks", ptTasksApiRouter);
api.use("/pt-task-phases", ptTaskPhasesApiRouter);
api.use("/pt-statuses", ptStatusesApiRouter);
api.use("/workload-gannt", workloadApiRouter);
api.use("/roadmap-gannt", roadmapApiRouter);
api.use("/schedule-gannt", scheduleApiRouter);
api.use("/schedule-gannt-v2", scheduleApiV2Router);
api.use("/project-managers", projectManagerApiRouter);
api.get("/overview/:id", safeControllerFunction(OverviewController.getById));
api.get("/task-priorities", safeControllerFunction(TaskPrioritiesController.get));
api.post("/change-password", passwordValidator, safeControllerFunction(AuthController.changePassword));
api.get("/access-controls/roles", safeControllerFunction(AccessControlsController.getRoles));
api.get("/logs/my-dashboard", safeControllerFunction(LogsController.getActivityLog));
api.use("/billing", billingApiRouter);
api.use("/task-dependencies", taskDependenciesApiRouter);
api.use("/task-recurring", taskRecurringApiRouter);
api.use("/custom-columns", customColumnsApiRouter);
api.use("/activity-logs", activityLogsApiRouter);
api.use("/project-activity-logs", projectActivityLogsApiRouter);
export default api;

View File

@@ -0,0 +1,22 @@
import express from "express";
import idParamValidator from "../../middlewares/validators/id-param-validator";
import safeControllerFunction from "../../shared/safe-controller-function";
import ProjectActivityLogsController from "../../controllers/project-activity-logs-controller";
const projectActivityLogsApiRouter = express.Router();
// Match the client URL (/project/:projectId) and ensure param name lines up
projectActivityLogsApiRouter.get(
"/project/:id",
// Validate that id is a proper UUID
idParamValidator,
// Wrap the controller to catch and forward errors
safeControllerFunction(ProjectActivityLogsController.getByProjectId)
);
// Optional healthcheck endpoint
projectActivityLogsApiRouter.get("/test", (req, res) => {
res.json({ message: "Project activity logs router is working!" });
});
export default projectActivityLogsApiRouter;