diff --git a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts index 4db8e3d5..c71e37b3 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts @@ -15,6 +15,25 @@ enum IToggleOptions { } export default class ReportingAllocationController extends ReportingControllerBase { + // Helper method to build billable query with custom table alias + private static buildBillableQueryWithAlias(selectedStatuses: { billable: boolean; nonBillable: boolean }, tableAlias: string = 'tasks'): string { + const { billable, nonBillable } = selectedStatuses; + + if (billable && nonBillable) { + // Both are enabled, no need to filter + return ""; + } else if (billable && !nonBillable) { + // Only billable is enabled - show only billable tasks + return ` AND ${tableAlias}.billable IS TRUE`; + } else if (!billable && nonBillable) { + // Only non-billable is enabled - show only non-billable tasks + return ` AND ${tableAlias}.billable IS FALSE`; + } else { + // Neither selected - this shouldn't happen in normal UI flow + return ""; + } + } + private static async getTimeLoggedByProjects(projects: string[], users: string[], key: string, dateRange: string[], archived = false, user_id = "", billable: { billable: boolean; nonBillable: boolean }): Promise { try { const projectIds = projects.map(p => `'${p}'`).join(","); @@ -77,8 +96,8 @@ export default class ReportingAllocationController extends ReportingControllerBa 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 ${billableQuery} - AND project_id = projects.id) AS all_tasks_count, + WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END + AND project_id = projects.id ${billableQuery}) AS all_tasks_count, (SELECT COUNT(*) FROM tasks WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END @@ -94,10 +113,11 @@ export default class ReportingAllocationController extends ReportingControllerBa SELECT name, (SELECT COALESCE(SUM(time_spent), 0) FROM task_work_log - LEFT JOIN tasks ON task_work_log.task_id = tasks.id - WHERE user_id = users.id ${billableQuery} + LEFT JOIN tasks ON task_work_log.task_id = tasks.id + WHERE user_id = users.id AND CASE WHEN ($1 IS TRUE) THEN tasks.project_id IS NOT NULL ELSE tasks.archived = FALSE END AND tasks.project_id = projects.id + ${billableQuery} ${duration}) AS time_logged FROM users WHERE id IN (${userIds}) @@ -121,10 +141,11 @@ export default class ReportingAllocationController extends ReportingControllerBa const q = `(SELECT id, (SELECT COALESCE(SUM(time_spent), 0) FROM task_work_log - LEFT JOIN tasks ON task_work_log.task_id = tasks.id ${billableQuery} + LEFT JOIN tasks ON task_work_log.task_id = tasks.id WHERE user_id = users.id AND CASE WHEN ($1 IS TRUE) THEN tasks.project_id IS NOT NULL ELSE tasks.archived = FALSE END AND tasks.project_id IN (${projectIds}) + ${billableQuery} ${duration}) AS time_logged FROM users WHERE id IN (${userIds}) @@ -346,6 +367,8 @@ export default class ReportingAllocationController extends ReportingControllerBa const projects = (req.body.projects || []) as string[]; const projectIds = projects.map(p => `'${p}'`).join(","); + const categories = (req.body.categories || []) as string[]; + const noCategory = req.body.noCategory || false; const billable = req.body.billable; if (!teamIds || !projectIds.length) @@ -361,6 +384,33 @@ export default class ReportingAllocationController extends ReportingControllerBa const billableQuery = this.buildBillableQuery(billable); + // Prepare projects filter + let projectsFilter = ""; + if (projectIds.length > 0) { + projectsFilter = `AND p.id IN (${projectIds})`; + } else { + // If no projects are selected, don't show any data + projectsFilter = `AND 1=0`; // This will match no rows + } + + // Prepare categories filter - updated logic + let categoriesFilter = ""; + if (categories.length > 0 && noCategory) { + // Both specific categories and "No Category" are selected + const categoryIds = categories.map(id => `'${id}'`).join(","); + categoriesFilter = `AND (p.category_id IS NULL OR p.category_id IN (${categoryIds}))`; + } else if (categories.length === 0 && noCategory) { + // Only "No Category" is selected + categoriesFilter = `AND p.category_id IS NULL`; + } else if (categories.length > 0 && !noCategory) { + // Only specific categories are selected + const categoryIds = categories.map(id => `'${id}'`).join(","); + categoriesFilter = `AND p.category_id IN (${categoryIds})`; + } else { + // categories.length === 0 && !noCategory - no categories selected, show nothing + categoriesFilter = `AND 1=0`; // This will match no rows + } + const q = ` SELECT p.id, p.name, @@ -368,13 +418,15 @@ export default class ReportingAllocationController extends ReportingControllerBa SUM(total_minutes) AS estimated, color_code FROM projects p - LEFT JOIN tasks ON tasks.project_id = p.id ${billableQuery} + LEFT JOIN tasks ON tasks.project_id = p.id LEFT JOIN task_work_log ON task_work_log.task_id = tasks.id - WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause} + WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause} ${categoriesFilter} ${billableQuery} GROUP BY p.id, p.name ORDER BY logged_time DESC;`; const result = await db.query(q, []); + const utilization = (req.body.utilization || []) as string[]; + const data = []; for (const project of result.rows) { @@ -401,10 +453,12 @@ export default class ReportingAllocationController extends ReportingControllerBa const projects = (req.body.projects || []) as string[]; const projectIds = projects.map(p => `'${p}'`).join(","); + const categories = (req.body.categories || []) as string[]; + const noCategory = req.body.noCategory || false; const billable = req.body.billable; - if (!teamIds || !projectIds.length) - return res.status(200).send(new ServerResponse(true, { users: [], projects: [] })); + if (!teamIds) + return res.status(200).send(new ServerResponse(true, { filteredRows: [], totals: { total_time_logs: "0", total_estimated_hours: "0", total_utilization: "0" } })); const { duration, date_range } = req.body; @@ -416,7 +470,9 @@ export default class ReportingAllocationController extends ReportingControllerBa endDate = moment(date_range[1]); } else if (duration === DATE_RANGES.ALL_TIME) { // Fetch the earliest start_date (or created_at if null) from selected projects - const minDateQuery = `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE id IN (${projectIds})`; + const minDateQuery = projectIds.length > 0 + ? `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE id IN (${projectIds})` + : `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE team_id IN (${teamIds})`; const minDateResult = await db.query(minDateQuery, []); const minDate = minDateResult.rows[0]?.min_date; startDate = minDate ? moment(minDate) : moment('2000-01-01'); @@ -445,59 +501,257 @@ export default class ReportingAllocationController extends ReportingControllerBa } } - // Count only weekdays (Mon-Fri) in the period + // Get organization working days + const orgWorkingDaysQuery = ` + SELECT monday, tuesday, wednesday, thursday, friday, saturday, sunday + FROM organization_working_days + WHERE organization_id IN ( + SELECT t.organization_id + FROM teams t + WHERE t.id IN (${teamIds}) + LIMIT 1 + ); + `; + const orgWorkingDaysResult = await db.query(orgWorkingDaysQuery, []); + const workingDaysConfig = orgWorkingDaysResult.rows[0] || { + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: false, + sunday: false + }; + + // Count working days based on organization settings let workingDays = 0; let current = startDate.clone(); while (current.isSameOrBefore(endDate, 'day')) { const day = current.isoWeekday(); - if (day >= 1 && day <= 5) workingDays++; + if ( + (day === 1 && workingDaysConfig.monday) || + (day === 2 && workingDaysConfig.tuesday) || + (day === 3 && workingDaysConfig.wednesday) || + (day === 4 && workingDaysConfig.thursday) || + (day === 5 && workingDaysConfig.friday) || + (day === 6 && workingDaysConfig.saturday) || + (day === 7 && workingDaysConfig.sunday) + ) { + workingDays++; + } current.add(1, 'day'); } - // Get hours_per_day for all selected projects - const projectHoursQuery = `SELECT id, hours_per_day FROM projects WHERE id IN (${projectIds})`; - const projectHoursResult = await db.query(projectHoursQuery, []); - const projectHoursMap: Record = {}; - for (const row of projectHoursResult.rows) { - projectHoursMap[row.id] = row.hours_per_day || 8; - } - // Sum total working hours for all selected projects - let totalWorkingHours = 0; - for (const pid of Object.keys(projectHoursMap)) { - totalWorkingHours += workingDays * projectHoursMap[pid]; + // Get organization working hours + const orgWorkingHoursQuery = `SELECT hours_per_day FROM organizations WHERE id = (SELECT t.organization_id FROM teams t WHERE t.id IN (${teamIds}) LIMIT 1)`; + const orgWorkingHoursResult = await db.query(orgWorkingHoursQuery, []); + const orgWorkingHours = orgWorkingHoursResult.rows[0]?.hours_per_day || 8; + + // Calculate total working hours with minimum baseline for non-working day scenarios + let totalWorkingHours = workingDays * orgWorkingHours; + let isNonWorkingPeriod = false; + + // If no working days but there might be logged time, set minimum baseline + // This ensures that time logged on non-working days is treated as over-utilization + // Business Logic: If someone works on weekends/holidays when workingDays = 0, + // we use a minimal baseline (1 hour) so any logged time results in >100% utilization + if (totalWorkingHours === 0) { + totalWorkingHours = 1; // Minimal baseline to ensure over-utilization + isNonWorkingPeriod = true; } - 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 billableQuery = this.buildBillableQuery(billable); + const billableQuery = this.buildBillableQueryWithAlias(billable, 't'); + const members = (req.body.members || []) as string[]; + + // Prepare members filter + let membersFilter = ""; + if (members.length > 0) { + const memberIds = members.map(id => `'${id}'`).join(","); + membersFilter = `AND tmiv.team_member_id IN (${memberIds})`; + } else { + // If no members are selected, we should not show any data + // This is different from other filters where no selection means "show all" + // For members, no selection should mean "show none" to respect the UI filter state + membersFilter = `AND 1=0`; // This will match no rows + } + // Note: Members filter works differently - when no members are selected, show nothing - 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 ON tasks.id = task_work_log.task_id ${billableQuery} - LEFT JOIN projects p ON p.id = tasks.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); - member.total_working_hours = totalWorkingHours; - member.utilization_percent = (totalWorkingHours > 0 && member.logged_time) ? ((parseFloat(member.logged_time) / (totalWorkingHours * 3600)) * 100).toFixed(2) : '0.00'; - member.utilized_hours = member.logged_time ? (parseFloat(member.logged_time) / 3600).toFixed(2) : '0.00'; - // Over/under utilized hours: utilized_hours - total_working_hours - const overUnder = member.utilized_hours && member.total_working_hours ? (parseFloat(member.utilized_hours) - member.total_working_hours) : 0; - member.over_under_utilized_hours = overUnder.toFixed(2); + // Create custom duration clause for twl table alias + let customDurationClause = ""; + if (date_range && date_range.length === 2) { + const start = moment(date_range[0]).format("YYYY-MM-DD"); + const end = moment(date_range[1]).format("YYYY-MM-DD"); + if (start === end) { + customDurationClause = `AND twl.created_at::DATE = '${start}'::DATE`; + } else { + customDurationClause = `AND twl.created_at::DATE >= '${start}'::DATE AND twl.created_at < '${end}'::DATE + INTERVAL '1 day'`; + } + } else { + const key = duration || DATE_RANGES.LAST_WEEK; + if (key === DATE_RANGES.YESTERDAY) + customDurationClause = "AND twl.created_at >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND twl.created_at < CURRENT_DATE::DATE"; + else if (key === DATE_RANGES.LAST_WEEK) + customDurationClause = "AND twl.created_at >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND twl.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'"; + else if (key === DATE_RANGES.LAST_MONTH) + customDurationClause = "AND twl.created_at >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND twl.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'"; + else if (key === DATE_RANGES.LAST_QUARTER) + customDurationClause = "AND twl.created_at >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND twl.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'"; } - return res.status(200).send(new ServerResponse(true, result.rows)); + // Prepare conditional filters for the subquery - only apply if selections are made + let conditionalProjectsFilter = ""; + let conditionalCategoriesFilter = ""; + + // Only apply project filter if projects are actually selected + if (projectIds.length > 0) { + conditionalProjectsFilter = `AND p.id IN (${projectIds})`; + } + + // Only apply category filter if categories are selected or noCategory is true + if (categories.length > 0 && noCategory) { + const categoryIds = categories.map(id => `'${id}'`).join(","); + conditionalCategoriesFilter = `AND (p.category_id IS NULL OR p.category_id IN (${categoryIds}))`; + } else if (categories.length === 0 && noCategory) { + conditionalCategoriesFilter = `AND p.category_id IS NULL`; + } else if (categories.length > 0 && !noCategory) { + const categoryIds = categories.map(id => `'${id}'`).join(","); + conditionalCategoriesFilter = `AND p.category_id IN (${categoryIds})`; + } + // If no categories and no noCategory, don't filter by category (show all) + + // Check if all filters are unchecked (Clear All scenario) - return no data to avoid overwhelming UI + const hasProjectFilter = projectIds.length > 0; + const hasCategoryFilter = categories.length > 0 || noCategory; + const hasMemberFilter = members.length > 0; + // Note: We'll check utilization filter after the query since it's applied post-processing + + if (!hasProjectFilter && !hasCategoryFilter && !hasMemberFilter) { + // Still need to check utilization filter, but we'll do a quick check + const utilization = (req.body.utilization || []) as string[]; + const hasUtilizationFilter = utilization.length > 0; + + if (!hasUtilizationFilter) { + return res.status(200).send(new ServerResponse(true, { filteredRows: [], totals: { total_time_logs: "0", total_estimated_hours: "0", total_utilization: "0" } })); + } + } + + // Modified query to start from team members and calculate filtered time logs + // This query ensures ALL active team members are included, even if they have no logged time + const q = ` + SELECT + tmiv.team_member_id, + tmiv.email, + tmiv.name, + COALESCE( + (SELECT SUM(twl.time_spent) + FROM task_work_log twl + LEFT JOIN tasks t ON t.id = twl.task_id + LEFT JOIN projects p ON p.id = t.project_id + WHERE twl.user_id = tmiv.user_id + ${customDurationClause} + ${conditionalProjectsFilter} + ${conditionalCategoriesFilter} + ${archivedClause} + ${billableQuery} + AND p.team_id = tmiv.team_id + ), 0 + ) AS logged_time + FROM team_member_info_view tmiv + WHERE tmiv.team_id IN (${teamIds}) + AND tmiv.active = TRUE + ${membersFilter} + GROUP BY tmiv.email, tmiv.name, tmiv.team_member_id, tmiv.user_id, tmiv.team_id + ORDER BY logged_time DESC;`; + + const result = await db.query(q, []); + const utilization = (req.body.utilization || []) as string[]; + + // Precompute totalWorkingHours * 3600 for efficiency + const totalWorkingSeconds = totalWorkingHours * 3600; + + // calculate utilization state + for (let i = 0, len = result.rows.length; i < len; i++) { + const member = result.rows[i]; + const loggedSeconds = member.logged_time ? parseFloat(member.logged_time) : 0; + const utilizedHours = loggedSeconds / 3600; + + // For individual members, use the same logic as total calculation + let memberWorkingHours; + let utilizationPercent; + + if (isNonWorkingPeriod) { + // Non-working period: each member's expected working hours is 0 + memberWorkingHours = 0; + // Any time logged during non-working period is overtime + utilizationPercent = loggedSeconds > 0 ? 100 : 0; // Show 100+ as numeric 100 for consistency + } else { + // Normal working period + memberWorkingHours = totalWorkingHours; + utilizationPercent = memberWorkingHours > 0 && loggedSeconds + ? ((loggedSeconds / (memberWorkingHours * 3600)) * 100) + : 0; + } + const overUnder = utilizedHours - memberWorkingHours; + + member.value = utilizedHours ? parseFloat(utilizedHours.toFixed(2)) : 0; + member.color_code = getColor(member.name); + member.total_working_hours = memberWorkingHours; + member.utilization_percent = utilizationPercent.toFixed(2); + member.utilized_hours = utilizedHours.toFixed(2); + member.over_under_utilized_hours = overUnder.toFixed(2); + + if (utilizationPercent < 90) { + member.utilization_state = 'under'; + } else if (utilizationPercent <= 110) { + member.utilization_state = 'optimal'; + } else { + member.utilization_state = 'over'; + } + } + + // Apply utilization filter + let filteredRows; + if (utilization.length > 0) { + // Filter to only show selected utilization states + filteredRows = result.rows.filter(member => utilization.includes(member.utilization_state)); + } else { + // No utilization states selected + // If we reached here, it means at least one other filter was applied + // so we show all members (don't filter by utilization) + filteredRows = result.rows; + } + + // Calculate totals + const total_time_logs = filteredRows.reduce((sum, member) => sum + parseFloat(member.logged_time || '0'), 0); + + let total_estimated_hours; + let total_utilization; + + if (isNonWorkingPeriod) { + // Non-working period: expected capacity is 0 + total_estimated_hours = 0; + // Special handling for utilization on non-working days + total_utilization = total_time_logs > 0 ? "100+" : "0"; + } else { + // Normal working period calculation + total_estimated_hours = totalWorkingHours * filteredRows.length; + total_utilization = total_time_logs > 0 && total_estimated_hours > 0 + ? ((total_time_logs / (total_estimated_hours * 3600)) * 100).toFixed(1) + : '0'; + } + + return res.status(200).send(new ServerResponse(true, { + filteredRows, + totals: { + total_time_logs: ((total_time_logs / 3600).toFixed(2)).toString(), + total_estimated_hours: total_estimated_hours.toString(), + total_utilization: total_utilization.toString(), + }, + })); } @HandleExceptions() @@ -580,6 +834,9 @@ export default class ReportingAllocationController extends ReportingControllerBa const projects = (req.body.projects || []) as string[]; const projectIds = projects.map(p => `'${p}'`).join(","); + + const categories = (req.body.categories || []) as string[]; + const noCategory = req.body.noCategory || false; const { type, billable } = req.body; if (!teamIds || !projectIds.length) @@ -595,6 +852,33 @@ export default class ReportingAllocationController extends ReportingControllerBa const billableQuery = this.buildBillableQuery(billable); + // Prepare projects filter + let projectsFilter = ""; + if (projectIds.length > 0) { + projectsFilter = `AND p.id IN (${projectIds})`; + } else { + // If no projects are selected, don't show any data + projectsFilter = `AND 1=0`; // This will match no rows + } + + // Prepare categories filter - updated logic + let categoriesFilter = ""; + if (categories.length > 0 && noCategory) { + // Both specific categories and "No Category" are selected + const categoryIds = categories.map(id => `'${id}'`).join(","); + categoriesFilter = `AND (p.category_id IS NULL OR p.category_id IN (${categoryIds}))`; + } else if (categories.length === 0 && noCategory) { + // Only "No Category" is selected + categoriesFilter = `AND p.category_id IS NULL`; + } else if (categories.length > 0 && !noCategory) { + // Only specific categories are selected + const categoryIds = categories.map(id => `'${id}'`).join(","); + categoriesFilter = `AND p.category_id IN (${categoryIds})`; + } else { + // categories.length === 0 && !noCategory - no categories selected, show nothing + categoriesFilter = `AND 1=0`; // This will match no rows + } + const q = ` SELECT p.id, p.name, @@ -608,9 +892,9 @@ export default class ReportingAllocationController extends ReportingControllerBa WHERE project_id = p.id) AS estimated, color_code FROM projects p - LEFT JOIN tasks ON tasks.project_id = p.id ${billableQuery} + LEFT JOIN tasks ON tasks.project_id = p.id LEFT JOIN task_work_log ON task_work_log.task_id = tasks.id - WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause} + WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause} ${categoriesFilter} ${billableQuery} GROUP BY p.id, p.name ORDER BY logged_time DESC;`; const result = await db.query(q, []); @@ -636,4 +920,4 @@ export default class ReportingAllocationController extends ReportingControllerBa return res.status(200).send(new ServerResponse(true, data)); } -} +} \ No newline at end of file diff --git a/worklenz-backend/src/controllers/reporting/reporting-members-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-members-controller.ts index 07fa6aae..64d1f483 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-members-controller.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-members-controller.ts @@ -10,9 +10,9 @@ import ReportingControllerBase from "./reporting-controller-base"; import Excel from "exceljs"; export default class ReportingMembersController extends ReportingControllerBase { + private static async getMembers( - teamId: string, - searchQuery = "", + teamId: string, searchQuery = "", size: number | null = null, offset: number | null = null, teamsClause = "", @@ -21,30 +21,17 @@ export default class ReportingMembersController extends ReportingControllerBase includeArchived: boolean, userId: string ) { - const pagingClause = - size !== null && offset !== null ? `LIMIT ${size} OFFSET ${offset}` : ""; + const pagingClause = (size !== null && offset !== null) ? `LIMIT ${size} OFFSET ${offset}` : ""; 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}')`; + ? "" + : `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 durationFilterClause = this.memberTasksDurationFilter(key, dateRange); const assignClause = this.memberAssignDurationFilter(key, dateRange); - const completedDurationClasue = this.completedDurationFilter( - key, - dateRange - ); - const overdueActivityLogsClause = this.getActivityLogsOverdue( - key, - dateRange - ); - const activityLogCreationFilter = this.getActivityLogsCreationClause( - key, - dateRange - ); - const timeLogDateRangeClause = this.getTimeLogDateRangeClause( - key, - dateRange - ); + const completedDurationClasue = this.completedDurationFilter(key, dateRange); + const overdueActivityLogsClause = this.getActivityLogsOverdue(key, dateRange); + const activityLogCreationFilter = this.getActivityLogsCreationClause(key, dateRange); + const timeLogDateRangeClause = this.getTimeLogDateRangeClause(key, dateRange); const q = `SELECT COUNT(DISTINCT email) AS total, (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON) @@ -123,11 +110,7 @@ export default class ReportingMembersController extends ReportingControllerBase AND t.billable IS TRUE AND t.project_id IN (SELECT id FROM projects WHERE team_id = $1) ${timeLogDateRangeClause} - ${ - 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}')` - }) AS billable_time, + ${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}')`}) AS billable_time, (SELECT COALESCE(SUM(twl.time_spent), 0) FROM task_work_log twl @@ -136,11 +119,7 @@ export default class ReportingMembersController extends ReportingControllerBase AND t.billable IS FALSE AND t.project_id IN (SELECT id FROM projects WHERE team_id = $1) ${timeLogDateRangeClause} - ${ - 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}')` - }) AS non_billable_time + ${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}')`}) AS non_billable_time FROM team_member_info_view tmiv WHERE tmiv.team_id = $1 ${teamsClause} ${searchQuery} @@ -157,30 +136,9 @@ export default class ReportingMembersController extends ReportingControllerBase for (const member of data.members) { member.color_code = getColor(member.name) + TASK_PRIORITY_COLOR_ALPHA; member.tasks_stat = { - todo: this.getPercentage( - int(member.todo_by_activity_logs), - +( - member.completed + - member.todo_by_activity_logs + - member.ongoing_by_activity_logs - ) - ), - doing: this.getPercentage( - int(member.ongoing_by_activity_logs), - +( - member.completed + - member.todo_by_activity_logs + - member.ongoing_by_activity_logs - ) - ), - done: this.getPercentage( - int(member.completed), - +( - member.completed + - member.todo_by_activity_logs + - member.ongoing_by_activity_logs - ) - ), + todo: this.getPercentage(int(member.todo_by_activity_logs), + (member.completed + member.todo_by_activity_logs + member.ongoing_by_activity_logs)), + doing: this.getPercentage(int(member.ongoing_by_activity_logs), + (member.completed + member.todo_by_activity_logs + member.ongoing_by_activity_logs)), + done: this.getPercentage(int(member.completed), + (member.completed + member.todo_by_activity_logs + member.ongoing_by_activity_logs)) }; member.member_teams = this.createTagList(member.member_teams, 2); } @@ -188,10 +146,7 @@ export default class ReportingMembersController extends ReportingControllerBase } private static flatString(text: string) { - return (text || "") - .split(" ") - .map((s) => `'${s}'`) - .join(","); + return (text || "").split(" ").map(s => `'${s}'`).join(","); } protected static memberTasksDurationFilter(key: string, dateRange: string[]) { @@ -218,10 +173,7 @@ export default class ReportingMembersController extends ReportingControllerBase return ""; } - protected static memberAssignDurationFilter( - key: string, - dateRange: string[] - ) { + 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"); @@ -270,6 +222,7 @@ export default class ReportingMembersController extends ReportingControllerBase } protected static getOverdueClause(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"); @@ -290,6 +243,7 @@ export default class ReportingMembersController extends ReportingControllerBase 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`; + return ` AND t.end_date::DATE < NOW()::DATE `; } @@ -329,6 +283,7 @@ export default class ReportingMembersController extends ReportingControllerBase } protected static getActivityLogsOverdue(key: string, dateRange: string[]) { + if (dateRange.length === 2) { const end = moment(dateRange[1]).format("YYYY-MM-DD"); return `AND is_overdue_for_date(t.id, '${end}'::DATE)`; @@ -337,10 +292,7 @@ export default class ReportingMembersController extends ReportingControllerBase return `AND is_overdue_for_date(t.id, NOW()::DATE)`; } - protected static getActivityLogsCreationClause( - key: string, - dateRange: string[] - ) { + 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`; @@ -348,11 +300,7 @@ export default class ReportingMembersController extends ReportingControllerBase return `AND tl.created_at::DATE <= NOW()::DATE`; } - protected static getDateRangeClauseMembers( - key: string, - dateRange: string[], - tableAlias: string - ) { + protected static getDateRangeClauseMembers(key: string, dateRange: string[], tableAlias: string) { if (dateRange.length === 2) { const start = moment(dateRange[0]).format("YYYY-MM-DD"); const end = moment(dateRange[1]).format("YYYY-MM-DD"); @@ -406,7 +354,7 @@ export default class ReportingMembersController extends ReportingControllerBase if (duration.asMilliseconds() === 0) return empty; - const h = ~~duration.asHours(); + const h = ~~(duration.asHours()); const m = duration.minutes(); const s = duration.seconds(); @@ -424,13 +372,8 @@ export default class ReportingMembersController extends ReportingControllerBase } @HandleExceptions() - public static async getReportingMembers( - req: IWorkLenzRequest, - res: IWorkLenzResponse - ): Promise { - const { searchQuery, size, offset } = this.toPaginationOptions(req.query, [ - "name", - ]); + public static async getReportingMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { searchQuery, size, offset } = this.toPaginationOptions(req.query, ["name"]); const { duration, date_range } = req.query; const archived = req.query.archived === "true"; @@ -439,29 +382,20 @@ export default class ReportingMembersController extends ReportingControllerBase dateRange = date_range.split(","); } - const teamsClause = (req.query.teams as string) - ? `AND tmiv.team_id IN (${this.flatString(req.query.teams as string)})` - : ""; + const teamsClause = + req.query.teams as string + ? `AND tmiv.team_id IN (${this.flatString(req.query.teams as string)})` + : ""; const teamId = this.getCurrentTeamId(req); - const result = await this.getMembers( - teamId as string, - searchQuery, - size, - offset, - teamsClause, - duration as string, - dateRange, - archived, - req.user?.id as string - ); + const result = await this.getMembers(teamId as string, searchQuery, size, offset, teamsClause, duration as string, dateRange, archived, req.user?.id as string); const body = { total: result.total, members: result.members, team: { id: req.user?.team_id, - name: req.user?.team_name, - }, + name: req.user?.team_name + } }; return res.status(200).send(new ServerResponse(true, body)); } @@ -486,28 +420,14 @@ export default class ReportingMembersController extends ReportingControllerBase const teamId = this.getCurrentTeamId(req); const teamName = (req.query.team_name as string)?.trim() || null; - const result = await this.getMembers( - teamId as string, - "", - null, - null, - "", - duration as string, - dateRange, - archived, - req.user?.id as string - ); + const result = await this.getMembers(teamId as string, "", null, null, "", duration as string, dateRange, archived, req.user?.id as string); 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() - : "-"; + 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: @@ -520,10 +440,7 @@ export default class ReportingMembersController extends ReportingControllerBase 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(); + start = moment().subtract(3, "months").format("YYYY-MM-DD").toString(); break; } end = moment().format("YYYY-MM-DD").toString(); @@ -544,36 +461,24 @@ export default class ReportingMembersController extends ReportingControllerBase { header: "Completed Tasks", key: "completed_tasks", width: 20 }, { header: "Ongoing Tasks", key: "ongoing_tasks", width: 20 }, { header: "Billable Time (seconds)", key: "billable_time", width: 25 }, - { - header: "Non-Billable Time (seconds)", - key: "non_billable_time", - width: 25, - }, + { header: "Non-Billable Time (seconds)", key: "non_billable_time", width: 25 }, { 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: "Todo Tasks(%)", key: "todo_tasks", width: 20 } ]; // set title sheet.getCell("A1").value = `Members from ${teamName}`; sheet.mergeCells("A1:M1"); sheet.getCell("A1").alignment = { horizontal: "center" }; - sheet.getCell("A1").style.fill = { - type: "pattern", - pattern: "solid", - fgColor: { argb: "D9D9D9" }, - }; + 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:M2"); sheet.getCell("A2").alignment = { horizontal: "center" }; - sheet.getCell("A2").style.fill = { - type: "pattern", - pattern: "solid", - fgColor: { argb: "F2F2F2" }, - }; + sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } }; sheet.getCell("A2").font = { size: 12 }; // set duration @@ -581,19 +486,7 @@ export default class ReportingMembersController extends ReportingControllerBase sheet.mergeCells("A3:D3"); // set table headers - sheet.getRow(5).values = [ - "Member", - "Email", - "Tasks Assigned", - "Overdue Tasks", - "Completed Tasks", - "Ongoing Tasks", - "Billable Time (seconds)", - "Non-Billable Time (seconds)", - "Done Tasks(%)", - "Doing Tasks(%)", - "Todo Tasks(%)", - ]; + sheet.getRow(5).values = ["Member", "Email", "Tasks Assigned", "Overdue Tasks", "Completed Tasks", "Ongoing Tasks", "Billable Time (seconds)", "Non-Billable Time (seconds)", "Done Tasks(%)", "Doing Tasks(%)", "Todo Tasks(%)"]; sheet.getRow(5).font = { bold: true }; for (const member of result.members) { @@ -608,27 +501,24 @@ export default class ReportingMembersController extends ReportingControllerBase non_billable_time: member.non_billable_time || 0, done_tasks: member.completed, doing_tasks: member.ongoing_by_activity_logs, - todo_tasks: member.todo_by_activity_logs, + todo_tasks: member.todo_by_activity_logs }); } // download excel res.setHeader("Content-Type", "application/vnd.openxmlformats"); - res.setHeader( - "Content-Disposition", - `attachment; filename=${fileName}.xlsx` - ); + res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`); + + await workbook.xlsx.write(res) + .then(() => { + res.end(); + }); - await workbook.xlsx.write(res).then(() => { - res.end(); - }); } @HandleExceptions() - public static async exportTimeLogs( - req: IWorkLenzRequest, - res: IWorkLenzResponse - ) { + public static async exportTimeLogs(req: IWorkLenzRequest, res: IWorkLenzResponse) { + const { duration, date_range, team_id, team_member_id } = req.query; const includeArchived = req.query.archived === "true"; @@ -638,37 +528,18 @@ export default class ReportingMembersController extends ReportingControllerBase dateRange = date_range.split(","); } - const durationClause = ReportingMembersController.getDateRangeClauseMembers( - (duration as string) || DATE_RANGES.LAST_WEEK, - dateRange, - "twl" - ); - const minMaxDateClause = this.getMinMaxDates( - (duration as string) || DATE_RANGES.LAST_WEEK, - dateRange, - "task_work_log" - ); + const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "twl"); + const minMaxDateClause = this.getMinMaxDates(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "task_work_log"); const memberName = (req.query.member_name as string)?.trim() || null; - const logGroups = await this.memberTimeLogsData( - durationClause, - minMaxDateClause, - team_id as string, - team_member_id as string, - includeArchived, - req.user?.id as string - ); + const logGroups = await this.memberTimeLogsData(durationClause, minMaxDateClause, team_id as string, team_member_id as string, includeArchived, req.user?.id as string); 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() - : "-"; + 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: @@ -681,15 +552,13 @@ export default class ReportingMembersController extends ReportingControllerBase 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(); + start = moment().subtract(3, "months").format("YYYY-MM-DD").toString(); break; } end = moment().format("YYYY-MM-DD").toString(); } + const exportDate = moment().format("MMM-DD-YYYY"); const fileName = `${memberName} timelogs - ${exportDate}`; const workbook = new Excel.Workbook(); @@ -704,22 +573,14 @@ export default class ReportingMembersController extends ReportingControllerBase sheet.getCell("A1").value = `Timelogs of ${memberName}`; sheet.mergeCells("A1:K1"); sheet.getCell("A1").alignment = { horizontal: "center" }; - sheet.getCell("A1").style.fill = { - type: "pattern", - pattern: "solid", - fgColor: { argb: "D9D9D9" }, - }; + 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:K2"); sheet.getCell("A2").alignment = { horizontal: "center" }; - sheet.getCell("A2").style.fill = { - type: "pattern", - pattern: "solid", - fgColor: { argb: "F2F2F2" }, - }; + sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } }; sheet.getCell("A2").font = { size: 12 }; // set duration @@ -734,27 +595,25 @@ export default class ReportingMembersController extends ReportingControllerBase for (const log of row.logs) { sheet.addRow({ date: row.log_day, - log: `Logged ${log.time_spent_string} for ${log.task_name} in ${log.project_name}`, + log: `Logged ${log.time_spent_string} for ${log.task_name} in ${log.project_name}` }); } } res.setHeader("Content-Type", "application/vnd.openxmlformats"); - res.setHeader( - "Content-Disposition", - `attachment; filename=${fileName}.xlsx` - ); + res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`); + + await workbook.xlsx.write(res) + .then(() => { + res.end(); + }); + - await workbook.xlsx.write(res).then(() => { - res.end(); - }); } @HandleExceptions() - public static async exportActivityLogs( - req: IWorkLenzRequest, - res: IWorkLenzResponse - ) { + public static async exportActivityLogs(req: IWorkLenzRequest, res: IWorkLenzResponse) { + const { duration, date_range, team_id, team_member_id } = req.query; const includeArchived = req.query.archived === "true"; @@ -763,37 +622,18 @@ export default class ReportingMembersController extends ReportingControllerBase dateRange = date_range.split(","); } - const durationClause = ReportingMembersController.getDateRangeClauseMembers( - (duration as string) || DATE_RANGES.LAST_WEEK, - dateRange, - "tal" - ); - const minMaxDateClause = this.getMinMaxDates( - (duration as string) || DATE_RANGES.LAST_WEEK, - dateRange, - "task_activity_logs" - ); + const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "tal"); + const minMaxDateClause = this.getMinMaxDates(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "task_activity_logs"); const memberName = (req.query.member_name as string)?.trim() || null; - const logGroups = await this.memberActivityLogsData( - durationClause, - minMaxDateClause, - team_id as string, - team_member_id as string, - includeArchived, - req.user?.id as string - ); + const logGroups = await this.memberActivityLogsData(durationClause, minMaxDateClause, team_id as string, team_member_id as string, includeArchived, req.user?.id as string); 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() - : "-"; + 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: @@ -806,15 +646,13 @@ export default class ReportingMembersController extends ReportingControllerBase 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(); + start = moment().subtract(3, "months").format("YYYY-MM-DD").toString(); break; } end = moment().format("YYYY-MM-DD").toString(); } + const exportDate = moment().format("MMM-DD-YYYY"); const fileName = `${memberName} activitylogs - ${exportDate}`; const workbook = new Excel.Workbook(); @@ -829,22 +667,14 @@ export default class ReportingMembersController extends ReportingControllerBase sheet.getCell("A1").value = `Activities of ${memberName}`; sheet.mergeCells("A1:K1"); sheet.getCell("A1").alignment = { horizontal: "center" }; - sheet.getCell("A1").style.fill = { - type: "pattern", - pattern: "solid", - fgColor: { argb: "D9D9D9" }, - }; + 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:K2"); sheet.getCell("A2").alignment = { horizontal: "center" }; - sheet.getCell("A2").style.fill = { - type: "pattern", - pattern: "solid", - fgColor: { argb: "F2F2F2" }, - }; + sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } }; sheet.getCell("A2").font = { size: 12 }; // set duration @@ -857,8 +687,8 @@ export default class ReportingMembersController extends ReportingControllerBase for (const row of logGroups) { for (const log of row.logs) { - !log.previous ? (log.previous = "NULL") : log.previous; - !log.current ? (log.current = "NULL") : log.current; + !log.previous ? log.previous = "NULL" : log.previous; + !log.current ? log.current = "NULL" : log.current; switch (log.attribute_type) { case "start_date": log.attribute_type = "Start Date"; @@ -880,29 +710,24 @@ export default class ReportingMembersController extends ReportingControllerBase } sheet.addRow({ date: row.log_day, - log: `Updated ${log.attribute_type} from ${log.previous} to ${log.current} in ${log.task_name} within ${log.project_name}.`, + log: `Updated ${log.attribute_type} from ${log.previous} to ${log.current} in ${log.task_name} within ${log.project_name}.` }); } } res.setHeader("Content-Type", "application/vnd.openxmlformats"); - res.setHeader( - "Content-Disposition", - `attachment; filename=${fileName}.xlsx` - ); + res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`); + + await workbook.xlsx.write(res) + .then(() => { + res.end(); + }); - await workbook.xlsx.write(res).then(() => { - res.end(); - }); } - public static async getMemberProjectsData( - teamId: string, - teamMemberId: string, - searchQuery: string, - archived: boolean, - userId: string - ) { + + public static async getMemberProjectsData(teamId: string, teamMemberId: string, searchQuery: string, archived: boolean, userId: string) { + const teamClause = teamId ? `team_member_id = '${teamMemberId as string}'` : `team_member_id IN (SELECT team_member_id @@ -911,9 +736,7 @@ export default class ReportingMembersController extends ReportingControllerBase FROM team_member_info_view tmiv2 WHERE tmiv2.team_member_id = '${teamMemberId}' AND in_organization(p.team_id, tmiv2.team_id)))`; - const archivedClause = archived - ? `` - : ` AND pm.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = pm.project_id AND user_id = '${userId}')`; + const archivedClause = archived ? `` : ` AND pm.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = pm.project_id AND user_id = '${userId}')`; const q = `SELECT p.id, p.name, pm.team_member_id, (SELECT name FROM teams WHERE id = p.team_id) AS team, @@ -957,43 +780,26 @@ export default class ReportingMembersController extends ReportingControllerBase const result = await db.query(q, []); for (const project of result.rows) { - project.time_logged = formatDuration( - moment.duration(project.time_logged, "seconds") - ); - project.contribution = - project.project_task_count > 0 - ? ((project.task_count / project.project_task_count) * 100).toFixed(0) - : 0; + project.time_logged = formatDuration(moment.duration(project.time_logged, "seconds")); + project.contribution = project.project_task_count > 0 ? ((project.task_count / project.project_task_count) * 100).toFixed(0) : 0; } return result.rows; } @HandleExceptions() - public static async getMemberProjects( - req: IWorkLenzRequest, - res: IWorkLenzResponse - ): Promise { + public static async getMemberProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const { searchQuery } = this.toPaginationOptions(req.query, ["p.name"]); const { teamMemberId, teamId } = req.query; const archived = req.query.archived === "true"; - const result = await this.getMemberProjectsData( - teamId as string, - teamMemberId as string, - searchQuery, - archived, - req.user?.id as string - ); + const result = await this.getMemberProjectsData(teamId as string, teamMemberId as string, searchQuery, archived, req.user?.id as string); return res.status(200).send(new ServerResponse(true, result)); } - protected static getMinMaxDates( - key: string, - dateRange: string[], - tableName: string - ) { + + protected static getMinMaxDates(key: string, dateRange: string[], tableName: string) { if (dateRange.length === 2) { const start = moment(dateRange[0]).format("YYYY-MM-DD"); const end = moment(dateRange[1]).format("YYYY-MM-DD"); @@ -1014,42 +820,22 @@ export default class ReportingMembersController extends ReportingControllerBase return ""; } + + @HandleExceptions() - public static async getMemberActivities( - req: IWorkLenzRequest, - res: IWorkLenzResponse - ): Promise { - const { team_member_id, team_id, duration, date_range, archived } = - req.body; + public static async getMemberActivities(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { team_member_id, team_id, duration, date_range, archived } = req.body; - const durationClause = ReportingMembersController.getDateRangeClauseMembers( - duration || DATE_RANGES.LAST_WEEK, - date_range, - "tal" - ); - const minMaxDateClause = this.getMinMaxDates( - duration || DATE_RANGES.LAST_WEEK, - date_range, - "task_activity_logs" - ); + const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration || DATE_RANGES.LAST_WEEK, date_range, "tal"); + const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range, "task_activity_logs"); - const logGroups = await this.memberActivityLogsData( - durationClause, - minMaxDateClause, - team_id, - team_member_id, - archived, - req.user?.id as string - ); + const logGroups = await this.memberActivityLogsData(durationClause, minMaxDateClause, team_id, team_member_id, archived, req.user?.id as string); return res.status(200).send(new ServerResponse(true, logGroups)); } - private static async formatLog(result: { - start_date: string; - end_date: string; - time_logs: any[]; - }) { + private static async formatLog(result: { start_date: string, end_date: string, time_logs: any[] }) { + result.time_logs.forEach((row) => { const duration = moment.duration(row.time_spent, "seconds"); row.time_spent_string = this.formatDuration(duration); @@ -1058,18 +844,10 @@ export default class ReportingMembersController extends ReportingControllerBase return result; } - private static async getTimeLogDays(result: { - start_date: string; - end_date: string; - time_logs: any[]; - }) { + private static async getTimeLogDays(result: { start_date: string, end_date: string, time_logs: any[] }) { if (result) { - const startDate = moment(result.start_date).isValid() - ? moment(result.start_date, "YYYY-MM-DD").clone() - : null; - const endDate = moment(result.end_date).isValid() - ? moment(result.end_date, "YYYY-MM-DD").clone() - : null; + const startDate = moment(result.start_date).isValid() ? moment(result.start_date, "YYYY-MM-DD").clone() : null; + const endDate = moment(result.end_date).isValid() ? moment(result.end_date, "YYYY-MM-DD").clone() : null; const days = []; const logDayGroups = []; @@ -1080,36 +858,25 @@ export default class ReportingMembersController extends ReportingControllerBase } for (const day of days) { - const logsForDay = result.time_logs.filter((log) => - moment(moment(log.created_at).format("YYYY-MM-DD")).isSame( - moment(day).format("YYYY-MM-DD") - ) - ); + const logsForDay = result.time_logs.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, + logs: logsForDay }); } } return logDayGroups; + } return []; } - private static async getActivityLogDays(result: { - start_date: string; - end_date: string; - activity_logs: any[]; - }) { + private static async getActivityLogDays(result: { start_date: string, end_date: string, activity_logs: any[] }) { if (result) { - const startDate = moment(result.start_date).isValid() - ? moment(result.start_date, "YYYY-MM-DD").clone() - : null; - const endDate = moment(result.end_date).isValid() - ? moment(result.end_date, "YYYY-MM-DD").clone() - : null; + const startDate = moment(result.start_date).isValid() ? moment(result.start_date, "YYYY-MM-DD").clone() : null; + const endDate = moment(result.end_date).isValid() ? moment(result.end_date, "YYYY-MM-DD").clone() : null; const days = []; const logDayGroups = []; @@ -1120,36 +887,27 @@ export default class ReportingMembersController extends ReportingControllerBase } for (const day of days) { - const logsForDay = result.activity_logs.filter((log) => - moment(moment(log.created_at).format("YYYY-MM-DD")).isSame( - moment(day).format("YYYY-MM-DD") - ) - ); + const logsForDay = result.activity_logs.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, + logs: logsForDay }); } } return logDayGroups; + } return []; } - private static async memberTimeLogsData( - durationClause: string, - minMaxDateClause: string, - team_id: string, - team_member_id: string, - includeArchived: boolean, - userId: string, - billableQuery = "" - ) { + + private static async memberTimeLogsData(durationClause: string, minMaxDateClause: string, team_id: string, team_member_id: string, includeArchived: boolean, userId: string, billableQuery = "") { + const archivedClause = includeArchived - ? "" - : `AND project_id NOT IN (SELECT project_id FROM archived_projects WHERE archived_projects.user_id = '${userId}')`; + ? "" + : `AND project_id NOT IN (SELECT project_id FROM archived_projects WHERE archived_projects.user_id = '${userId}')`; const q = ` SELECT user_id, @@ -1190,17 +948,9 @@ export default class ReportingMembersController extends ReportingControllerBase return logGroups; } - private static async memberActivityLogsData( - durationClause: string, - minMaxDateClause: string, - team_id: string, - team_member_id: string, - includeArchived: boolean, - userId: string - ) { - const archivedClause = includeArchived - ? `` - : `AND (SELECT project_id FROM tasks WHERE id = tal.task_id) NOT IN (SELECT project_id FROM archived_projects WHERE archived_projects.user_id = '${userId}')`; + private static async memberActivityLogsData(durationClause: string, minMaxDateClause: string, team_id: string, team_member_id: string, includeArchived:boolean, userId: string) { + + const archivedClause = includeArchived ? `` : `AND (SELECT project_id FROM tasks WHERE id = tal.task_id) NOT IN (SELECT project_id FROM archived_projects WHERE archived_projects.user_id = '${userId}')`; const q = ` SELECT user_id, @@ -1305,14 +1055,12 @@ export default class ReportingMembersController extends ReportingControllerBase } return logGroups; + } - protected static buildBillableQuery(selectedStatuses: { - billable: boolean; - nonBillable: boolean; - }): string { + protected static buildBillableQuery(selectedStatuses: { billable: boolean; nonBillable: boolean }): string { const { billable, nonBillable } = selectedStatuses; - + if (billable && nonBillable) { // Both are enabled, no need to filter return ""; @@ -1322,56 +1070,28 @@ export default class ReportingMembersController extends ReportingControllerBase } else if (nonBillable) { // Only non-billable is enabled return " AND tasks.billable IS FALSE"; - } + } return ""; } @HandleExceptions() - public static async getMemberTimelogs( - req: IWorkLenzRequest, - res: IWorkLenzResponse - ): Promise { - const { - team_member_id, - team_id, - duration, - date_range, - archived, - billable, - } = req.body; + public static async getMemberTimelogs(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { team_member_id, team_id, duration, date_range, archived, billable } = req.body; - const durationClause = ReportingMembersController.getDateRangeClauseMembers( - duration || DATE_RANGES.LAST_WEEK, - date_range, - "twl" - ); - const minMaxDateClause = this.getMinMaxDates( - duration || DATE_RANGES.LAST_WEEK, - date_range, - "task_work_log" - ); + const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration || DATE_RANGES.LAST_WEEK, date_range, "twl"); + const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range, "task_work_log"); const billableQuery = this.buildBillableQuery(billable); - const logGroups = await this.memberTimeLogsData( - durationClause, - minMaxDateClause, - team_id, - team_member_id, - archived, - req.user?.id as string, - billableQuery - ); + const logGroups = await this.memberTimeLogsData(durationClause, minMaxDateClause, team_id, team_member_id, archived, req.user?.id as string, billableQuery); return res.status(200).send(new ServerResponse(true, logGroups)); } @HandleExceptions() - public static async getMemberTaskStats( - req: IWorkLenzRequest, - res: IWorkLenzResponse - ): Promise { + public static async getMemberTaskStats(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { duration, date_range, team_member_id } = req.query; const includeArchived = req.query.archived === "true"; @@ -1381,26 +1101,15 @@ export default class ReportingMembersController extends ReportingControllerBase } 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 = '${req.user?.id}')`; + ? "" + : `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${req.user?.id}')`; - const assignClause = this.memberAssignDurationFilter( - duration as string, - dateRange - ); - const completedDurationClasue = this.completedDurationFilter( - duration as string, - dateRange - ); - const overdueClauseByDate = this.getActivityLogsOverdue( - duration as string, - dateRange - ); + + const assignClause = this.memberAssignDurationFilter(duration as string, dateRange); + const completedDurationClasue = this.completedDurationFilter(duration as string, dateRange); + const overdueClauseByDate = this.getActivityLogsOverdue(duration as string, dateRange); const taskSelectorClause = this.getTaskSelectorClause(); - const durationFilter = this.memberTasksDurationFilter( - duration as string, - dateRange - ); + const durationFilter = this.memberTasksDurationFilter(duration as string, dateRange); const q = ` SELECT name AS team_member_name, @@ -1445,12 +1154,7 @@ export default class ReportingMembersController extends ReportingControllerBase const [data] = result.rows; if (data) { - for (const taskArray of [ - data.assigned, - data.completed, - data.ongoing, - data.overdue, - ]) { + for (const taskArray of [data.assigned, data.completed, data.ongoing, data.overdue]) { this.updateTaskProperties(taskArray); } } @@ -1461,29 +1165,29 @@ export default class ReportingMembersController extends ReportingControllerBase { name: "Total Tasks", color_code: "#7590c9", - tasks: data.total ? data.total : 0, + tasks: data.total ? data.total : 0 }, { name: "Tasks Assigned", color_code: "#7590c9", - tasks: data.assigned ? data.assigned : 0, + tasks: data.assigned ? data.assigned : 0 }, { name: "Tasks Completed", color_code: "#75c997", - tasks: data.completed ? data.completed : 0, + tasks: data.completed ? data.completed : 0 }, { name: "Tasks Overdue", color_code: "#eb6363", - tasks: data.overdue ? data.overdue : 0, + tasks: data.overdue ? data.overdue : 0 }, { name: "Tasks Ongoing", color_code: "#7cb5ec", - tasks: data.ongoing ? data.ongoing : 0, + tasks: data.ongoing ? data.ongoing : 0 }, - ], + ] }; return res.status(200).send(new ServerResponse(true, body)); @@ -1491,33 +1195,24 @@ export default class ReportingMembersController extends ReportingControllerBase private static updateTaskProperties(tasks: any[]) { for (const task of tasks) { - task.project_color = getColor(task.project_name); - task.estimated_string = formatDuration( - moment.duration(~~task.total_minutes, "seconds") - ); - task.time_spent_string = formatDuration( - moment.duration(~~task.time_logged, "seconds") - ); - task.overlogged_time_string = formatDuration( - moment.duration(~~task.overlogged_time, "seconds") - ); - task.overdue_days = task.days_overdue ? task.days_overdue : null; + task.project_color = getColor(task.project_name); + task.estimated_string = formatDuration(moment.duration(~~(task.total_minutes), "seconds")); + task.time_spent_string = formatDuration(moment.duration(~~(task.time_logged), "seconds")); + task.overlogged_time_string = formatDuration(moment.duration(~~(task.overlogged_time), "seconds")); + task.overdue_days = task.days_overdue ? task.days_overdue : null; } - } +} - @HandleExceptions() - public static async getSingleMemberProjects( - req: IWorkLenzRequest, - res: IWorkLenzResponse - ) { - const { team_member_id } = req.query; - const includeArchived = req.query.archived === "true"; +@HandleExceptions() +public static async getSingleMemberProjects(req: IWorkLenzRequest, res: IWorkLenzResponse) { + const { team_member_id } = req.query; + 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 archived_projects.user_id = '${req.user?.id}')`; + const archivedClause = includeArchived + ? "" + : `AND projects.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, + const q = `SELECT id, name, color_code, start_date, @@ -1568,60 +1263,46 @@ export default class ReportingMembersController extends ReportingControllerBase FROM projects WHERE projects.id IN (SELECT project_id FROM project_members WHERE team_member_id = $1) ${archivedClause};`; - const result = await db.query(q, [team_member_id]); - const data = result.rows; + const result = await db.query(q, [team_member_id]); + const data = result.rows; - for (const row of data) { - row.estimated_time = int(row.estimated_time); - row.actual_time = int(row.actual_time); - row.estimated_time_string = this.convertMinutesToHoursAndMinutes( - int(row.estimated_time) - ); - row.actual_time_string = this.convertSecondsToHoursAndMinutes( - int(row.actual_time) - ); - row.days_left = ReportingControllerBase.getDaysLeft(row.end_date); - row.is_overdue = ReportingControllerBase.isOverdue(row.end_date); - if (row.days_left && row.is_overdue) { - row.days_left = row.days_left.toString().replace(/-/g, ""); - } - row.is_today = this.isToday(row.end_date); - if (row.project_manager) { - row.project_manager.name = - row.project_manager.project_manager_info.name; - row.project_manager.avatar_url = - row.project_manager.project_manager_info.avatar_url; - row.project_manager.color_code = getColor(row.project_manager.name); - } - row.project_health = row.health_name ? row.health_name : null; + for (const row of data) { + row.estimated_time = int(row.estimated_time); + row.actual_time = int(row.actual_time); + row.estimated_time_string = this.convertMinutesToHoursAndMinutes(int(row.estimated_time)); + row.actual_time_string = this.convertSecondsToHoursAndMinutes(int(row.actual_time)); + row.days_left = ReportingControllerBase.getDaysLeft(row.end_date); + row.is_overdue = ReportingControllerBase.isOverdue(row.end_date); + if (row.days_left && row.is_overdue) { + row.days_left = row.days_left.toString().replace(/-/g, ""); } - - const body = { - team_member_name: data[0].team_member_name, - projects: data, - }; - - return res.status(200).send(new ServerResponse(true, body)); + row.is_today = this.isToday(row.end_date); + if (row.project_manager) { + row.project_manager.name = row.project_manager.project_manager_info.name; + row.project_manager.avatar_url = row.project_manager.project_manager_info.avatar_url; + row.project_manager.color_code = getColor(row.project_manager.name); + } + row.project_health = row.health_name ? row.health_name : null; } + const body = { + team_member_name: data[0].team_member_name, + projects: data + }; + + return res.status(200).send(new ServerResponse(true, body)); + +} + @HandleExceptions() - public static async exportMemberProjects( - req: IWorkLenzRequest, - res: IWorkLenzResponse - ) { + public static async exportMemberProjects(req: IWorkLenzRequest, res: IWorkLenzResponse) { const teamMemberId = (req.query.team_member_id as string)?.trim() || null; const teamId = (req.query.team_id as string)?.trim() || null; const memberName = (req.query.team_member_name as string)?.trim() || null; const teamName = (req.query.team_name as string)?.trim() || ""; const archived = req.query.archived === "true"; - const result = await this.getMemberProjectsData( - teamId as string, - teamMemberId as string, - "", - archived, - req.user?.id as string - ); + const result = await this.getMemberProjectsData(teamId as string, teamMemberId as string, "", archived, req.user?.id as string); // excel file const exportDate = moment().format("MMM-DD-YYYY"); @@ -1638,29 +1319,21 @@ export default class ReportingMembersController extends ReportingControllerBase { header: "Incompleted Tasks", key: "incompleted_tasks", width: 20 }, { header: "Completed Tasks", key: "completed_tasks", width: 20 }, { header: "Overdue Tasks", key: "overdue_tasks", width: 20 }, - { header: "Logged Time", key: "logged_time", width: 20 }, + { header: "Logged Time", key: "logged_time", width: 20 } ]; // set title sheet.getCell("A1").value = `Projects of ${memberName} - ${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").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").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } }; sheet.getCell("A2").font = { size: 12 }; // set duration @@ -1670,16 +1343,7 @@ export default class ReportingMembersController extends ReportingControllerBase // sheet.mergeCells("A3:D3"); // set table headers - sheet.getRow(4).values = [ - "Project", - "Team", - "Tasks", - "Contribution(%)", - "Incompleted Tasks", - "Completed Tasks", - "Overdue Tasks", - "Logged Time", - ]; + sheet.getRow(4).values = ["Project", "Team", "Tasks", "Contribution(%)", "Incompleted Tasks", "Completed Tasks", "Overdue Tasks", "Logged Time"]; sheet.getRow(4).font = { bold: true }; for (const project of result) { @@ -1687,27 +1351,23 @@ export default class ReportingMembersController extends ReportingControllerBase project: project.name, team: project.team, tasks: project.task_count ? project.task_count.toString() : "-", - contribution: project.contribution - ? project.contribution.toString() - : "-", - incompleted_tasks: project.incompleted - ? project.incompleted.toString() - : "-", + contribution: project.contribution ? project.contribution.toString() : "-", + incompleted_tasks: project.incompleted ? project.incompleted.toString() : "-", completed_tasks: project.completed ? project.completed.toString() : "-", overdue_tasks: project.overdue ? project.overdue.toString() : "-", - logged_time: project.time_logged ? project.time_logged.toString() : "-", + logged_time: project.time_logged ? project.time_logged.toString() : "-" }); } // download excel res.setHeader("Content-Type", "application/vnd.openxmlformats"); - res.setHeader( - "Content-Disposition", - `attachment; filename=${fileName}.xlsx` - ); + res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`); + + await workbook.xlsx.write(res) + .then(() => { + res.end(); + }); - await workbook.xlsx.write(res).then(() => { - res.end(); - }); } -} + +} \ No newline at end of file