Skip to content

Commit 9d56fc0

Browse files
authored
Merge pull request #370 from Worklenz/development
Development
2 parents 9db7b1c + b1e3547 commit 9d56fc0

12 files changed

Lines changed: 317 additions & 125 deletions

worklenz-backend/src/controllers/project-templates/pt-tasks-controller.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export default class PtTasksController extends PtTasksControllerBase {
3939
const searchField = options.search ? "cptt.name" : "sort_order";
4040
const { searchQuery, sortField } = PtTasksController.toPaginationOptions(options, searchField);
4141

42-
const sortFields = sortField.replace(/ascend/g, "ASC").replace(/descend/g, "DESC") || "sort_order";
42+
const sortFields = (sortField as string).replace(/ascend/g, "ASC").replace(/descend/g, "DESC") || "sort_order";
4343

4444
const isSubTasks = !!options.parent_task;
4545

@@ -230,13 +230,13 @@ export default class PtTasksController extends PtTasksControllerBase {
230230

231231
@HandleExceptions()
232232
public static async bulkDelete(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
233-
const deletedTasks = req.body.tasks.map((t: any) => t.id);
233+
const deletedTasks = req.body.tasks.map((t: any) => t.id);
234234

235-
const result: any = {deleted_tasks: deletedTasks};
235+
const result: any = { deleted_tasks: deletedTasks };
236236

237-
const q = `SELECT bulk_delete_pt_tasks($1) AS task;`;
238-
await db.query(q, [JSON.stringify(req.body)]);
239-
return res.status(200).send(new ServerResponse(true, result));
237+
const q = `SELECT bulk_delete_pt_tasks($1) AS task;`;
238+
await db.query(q, [JSON.stringify(req.body)]);
239+
return res.status(200).send(new ServerResponse(true, result));
240240
}
241241

242242
}

worklenz-backend/src/controllers/project-workload/workload-gannt-controller.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,10 @@ export default class WorkloadGanntController extends WLTasksControllerBase {
220220
if (logsRange.max_date)
221221
logsRange.max_date = momentTime.tz(logsRange.max_date, `${timeZone}`).format("YYYY-MM-DD");
222222

223-
if (moment(logsRange.min_date ).isBefore(dateRange.start_date))
223+
if (moment(logsRange.min_date).isBefore(dateRange.start_date))
224224
dateRange.start_date = logsRange.min_date;
225225

226-
if (moment(logsRange.max_date ).isAfter(dateRange.endDate))
226+
if (moment(logsRange.max_date).isAfter(dateRange.endDate))
227227
dateRange.end_date = logsRange.max_date;
228228

229229
return dateRange;
@@ -331,7 +331,7 @@ export default class WorkloadGanntController extends WLTasksControllerBase {
331331

332332
for (const member of result.rows) {
333333
member.color_code = getColor(member.name);
334-
334+
335335
// Set default working settings if organization data is not available
336336
member.org_working_hours = member.org_working_hours || 8;
337337
member.org_working_days = member.org_working_days || {
@@ -492,7 +492,7 @@ export default class WorkloadGanntController extends WLTasksControllerBase {
492492

493493
private static getFilterByDatesWhereClosure(dateChecker?: string): string {
494494
if (!dateChecker) return "";
495-
495+
496496
switch (dateChecker) {
497497
case this.TASKS_START_DATE_NULL_FILTER:
498498
return "start_date IS NULL";
@@ -540,7 +540,7 @@ export default class WorkloadGanntController extends WLTasksControllerBase {
540540
const queryParams: any[] = [];
541541
let paramOffset = 1;
542542

543-
const sortFields = sortField.replace(/ascend/g, "ASC").replace(/descend/g, "DESC") || "sort_order";
543+
const sortFields = (sortField as string).replace(/ascend/g, "ASC").replace(/descend/g, "DESC") || "sort_order";
544544
// Filter tasks by its members
545545
const membersResult = WorkloadGanntController.getFilterByMembersWhereClosure(options.members as string, paramOffset);
546546
if (membersResult.params.length > 0) {

worklenz-backend/src/controllers/reporting/overview/reporting-overview-controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export default class ReportingOverviewController extends ReportingOverviewBase {
9090

9191
const teamFilterClause = `p.team_id = $1`;
9292

93-
const result = await ReportingControllerBase.getProjectsByTeam(teamId, size, offset, searchQuery, sortField, sortOrder, "", "", "", archivedClause, teamFilterClause, "");
93+
const result = await ReportingControllerBase.getProjectsByTeam(teamId, size, offset, searchQuery, sortField as string, sortOrder, "", "", "", archivedClause, teamFilterClause, "");
9494

9595

9696
for (const project of result.projects) {

worklenz-backend/src/controllers/reporting/overview/reporting-overview-export-controller.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default class ReportingOverviewExportController extends ReportingOverview
2424

2525
const teamFilterClause = `p.team_id = $1`;
2626

27-
const result = await ReportingControllerBase.getProjectsByTeam(teamId, size, offset, searchQuery, sortField, sortOrder, "", "", "", archivedClause, teamFilterClause, "");
27+
const result = await ReportingControllerBase.getProjectsByTeam(teamId, size, offset, searchQuery, sortField as string, sortOrder, "", "", "", archivedClause, teamFilterClause, "");
2828

2929
for (const project of result.projects) {
3030
project.team_color = getColor(project.team_name) + TASK_PRIORITY_COLOR_ALPHA;
@@ -416,7 +416,7 @@ export default class ReportingOverviewExportController extends ReportingOverview
416416
const teamMemberName = (req.query.team_member_name as string)?.trim() || null;
417417
const teamName = (req.query.team_name as string)?.trim() || "";
418418

419-
const { duration, date_range, only_single_member, archived} = req.query;
419+
const { duration, date_range, only_single_member, archived } = req.query;
420420

421421
const includeArchived = req.query.archived === "true";
422422

@@ -506,7 +506,7 @@ export default class ReportingOverviewExportController extends ReportingOverview
506506

507507
const includeArchived = req.query.archived === "true";
508508

509-
const results = await ReportingExportModel.getMemberTasks(teamMemberId as string, projectId, "false", "", [], includeArchived, req.user?.id as string);
509+
const results = await ReportingExportModel.getMemberTasks(teamMemberId as string, projectId, "false", "", [], includeArchived, req.user?.id as string);
510510

511511
// excel file
512512
const exportDate = moment().format("MMM-DD-YYYY");

worklenz-backend/src/controllers/reporting/projects/reporting-projects-controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export default class ReportingProjectsController extends ReportingProjectsBase {
8080
const projectFilterClause = await this.buildProjectFilterForTeamLead(req);
8181
const teamFilterClause = `in_organization(p.team_id, $1) ${projectFilterClause} ${teamsClause}`;
8282

83-
const result = await ReportingControllerBase.getProjectsByTeam(teamId as string, size, offset, searchQuery, sortField, sortOrder, statusesClause, healthsClause, categoriesClause, archivedClause, teamFilterClause, projectManagersClause, filterParams);
83+
const result = await ReportingControllerBase.getProjectsByTeam(teamId as string, size, offset, searchQuery, sortField as string, sortOrder, statusesClause, healthsClause, categoriesClause, archivedClause, teamFilterClause, projectManagersClause, filterParams);
8484

8585
for (const project of result.projects) {
8686
project.team_color = getColor(project.team_name) + TASK_PRIORITY_COLOR_ALPHA;

worklenz-backend/src/controllers/reporting/reporting-controller-base.ts

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export default abstract class ReportingControllerBase extends WorklenzController
111111

112112
protected static buildBillableQuery(selectedStatuses: { billable: boolean; nonBillable: boolean }): string {
113113
const { billable, nonBillable } = selectedStatuses;
114-
114+
115115
if (billable && nonBillable) {
116116
// Both are enabled, no need to filter
117117
return "";
@@ -121,7 +121,7 @@ export default abstract class ReportingControllerBase extends WorklenzController
121121
} else if (nonBillable) {
122122
// Only non-billable is enabled
123123
return " AND tasks.billable IS FALSE";
124-
}
124+
}
125125

126126
return "";
127127
}
@@ -166,6 +166,67 @@ export default abstract class ReportingControllerBase extends WorklenzController
166166
}
167167

168168

169+
/**
170+
* Build project filter clause for Team Leads
171+
* Team Leads can only see projects they are assigned to as project managers
172+
*/
173+
public static async buildProjectFilterForTeamLead(req: IWorkLenzRequest): Promise<string> {
174+
// Check if user is a Team Lead (not Admin or Owner)
175+
const userId = req.user?.id;
176+
const teamId = req.user?.team_id;
177+
178+
if (!userId || !teamId) return "";
179+
180+
// Check user's role
181+
const roleQuery = `
182+
SELECT r.key
183+
FROM roles r
184+
JOIN team_members tm ON tm.role_id = r.id
185+
WHERE tm.user_id = $1 AND tm.team_id = $2
186+
`;
187+
const roleResult = await db.query(roleQuery, [userId, teamId]);
188+
189+
if (roleResult.rows.length === 0) return "";
190+
191+
const roleKey = roleResult.rows[0].key;
192+
193+
// Only apply filter for Team Leads
194+
if (roleKey === 'TEAM_LEAD') {
195+
// Team Leads can only see projects they manage
196+
return `AND p.id IN (
197+
SELECT pm.project_id
198+
FROM project_members pm
199+
WHERE pm.team_member_id IN (
200+
SELECT id FROM team_members WHERE user_id = '${userId}'
201+
)
202+
AND pm.project_access_level_id = (
203+
SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER'
204+
)
205+
)`;
206+
}
207+
208+
// Admins and Owners can see all projects
209+
return "";
210+
}
211+
212+
/**
213+
* Get project IDs that a Team Lead is assigned to
214+
*/
215+
public static async getTeamLeadProjects(userId: string, teamId: string): Promise<string[]> {
216+
const q = `
217+
SELECT DISTINCT pm.project_id
218+
FROM project_members pm
219+
JOIN team_members tm ON pm.team_member_id = tm.id
220+
WHERE tm.user_id = $1
221+
AND tm.team_id = $2
222+
AND pm.project_access_level_id = (
223+
SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER'
224+
)
225+
`;
226+
const result = await db.query(q, [userId, teamId]);
227+
return result.rows.map(row => row.project_id);
228+
}
229+
169230
public static async getProjectsByTeam(
170231
teamId: string,
171232
size: string | number | null,
@@ -178,7 +239,8 @@ export default abstract class ReportingControllerBase extends WorklenzController
178239
categoryClause: string,
179240
archivedClause = "",
180241
teamFilterClause: string,
181-
projectManagersClause: string) {
242+
projectManagersClause: string,
243+
filterParams: any[] = []) {
182244

183245
const q = `SELECT COUNT(*) AS total,
184246
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
@@ -315,7 +377,9 @@ export default abstract class ReportingControllerBase extends WorklenzController
315377
LEFT JOIN project_categories pc ON pc.id = p.category_id
316378
LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
317379
WHERE ${teamFilterClause} ${searchQuery} ${healthClause} ${statusClause} ${categoryClause} ${projectManagersClause} ${archivedClause};`;
318-
const result = await db.query(q, [teamId, size, offset]);
380+
// Combine all parameters: teamId, size, offset, then filter params
381+
const queryParams = [teamId, size, offset, ...filterParams];
382+
const result = await db.query(q, queryParams);
319383
const [data] = result.rows;
320384

321385
for (const project of data.projects) {

0 commit comments

Comments
 (0)