Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 8 additions & 69 deletions src/helpers/overviewReportHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -498,77 +498,16 @@
comparisonStartDate,
comparisonEndDate,
) {
// One definition of "active team", shared with Team Stats: an active team
// (created by the end date) with at least 2 currently active members.
// This keeps the Total Active Teams card equal to "N teams with 2+ active members".
const ACTIVE_TEAM_MIN_MEMBERS = 2;
const getActiveTeamCount = async (start, end) => {
// Convert dates to YYYY-MM-DD string format for comparison with dateOfWork
const startStr = moment(start).format('YYYY-MM-DD');
const endStr = moment(end).format('YYYY-MM-DD');

console.log(`\n[getTotalActiveTeamCount] ========== START ==========`);
console.log(`[getTotalActiveTeamCount] Processing date range: ${startStr} to ${endStr}`);
console.log(`[getTotalActiveTeamCount] Input dates - start: ${start}, end: ${end}`);

// Step 1: Get all active teams
const activeTeamsCount = await Team.countDocuments({ isActive: true });
console.log(`[getTotalActiveTeamCount] Total active teams (no filter): ${activeTeamsCount}`);

const result = await Team.aggregate([
// Step 1: Match active teams created before/on the end date
{
$match: {
isActive: true,
$or: [{ createdDatetime: { $exists: false } }, { createdDatetime: { $lte: end } }],
},
},
// Step 2: Lookup time entries for all team members
{
$lookup: {
from: 'timeEntries',
localField: 'members.userId',
foreignField: 'personId',
as: 'allTeamTimeEntries',
},
},
// Step 3: Filter the time entries to only those in the date range
{
$project: {
_id: 1,
teamName: 1,
isActive: 1,
memberCount: { $size: '$members' },
totalTimeEntriesCount: { $size: '$allTeamTimeEntries' },
// Filter time entries to only those within the date range
timeEntriesInRange: {
$filter: {
input: '$allTeamTimeEntries',
as: 'entry',
cond: {
$and: [
{ $gte: ['$$entry.dateOfWork', startStr] },
{ $lte: ['$$entry.dateOfWork', endStr] },
],
},
},
},
},
},
// Step 4: Keep only teams that have at least one time entry in the date range
{
$match: {
'timeEntriesInRange.0': { $exists: true },
},
},
// Step 5: Count the matching teams
{
$count: 'activeTeams',
},
]);

const activeTeamsWithHours = result[0]?.activeTeams || 0;
console.log(
`[getTotalActiveTeamCount] Teams with logged hours in range ${startStr} to ${endStr}: ${activeTeamsWithHours}`,
const { count } = await getTeamsWithActiveMembers(
moment(end).endOf('day').toDate(),
ACTIVE_TEAM_MIN_MEMBERS,
);
console.log(`[getTotalActiveTeamCount] ========== END ==========\n`);
return activeTeamsWithHours;
return count;
};

const current = await getActiveTeamCount(startDate, endDate);
Expand Down Expand Up @@ -1011,7 +950,7 @@
* NOTE: This shows ALL active users regardless of createdDate to provide
* a complete picture of current role distribution in the organization
*/
async function getRoleDistributionStats() {

Check warning on line 953 in src/helpers/overviewReportHelper.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move async function 'getRoleDistributionStats' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HGNRest&issues=AaEebqOzuxmVtMcBf8GT&open=AaEebqOzuxmVtMcBf8GT&pullRequest=2391
// Always match only active users, ignore date filters for role distribution
// This ensures all current roles are displayed, not just recently created users.
// Role distribution is a live snapshot, not a time-series metric, so comparison
Expand Down
49 changes: 49 additions & 0 deletions src/helpers/overviewReportHelper.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,52 @@ describe('overviewReportHelper tests', () => {
// });
// });
// });

describe('getTotalActiveTeamCount', () => {
// eslint-disable-next-line global-require
const Team = require('../models/team');

afterEach(() => {
jest.restoreAllMocks();
});

it('uses the Team Stats definition: active teams with 2+ active members', async () => {
const aggregateSpy = jest.spyOn(Team, 'aggregate').mockResolvedValue([{ totalTeams: 211 }]);
const { getTotalActiveTeamCount } = overviewReportHelper();

const result = await getTotalActiveTeamCount('2026-10-04', '2026-10-10');

expect(result).toEqual({ current: 211 });
expect(aggregateSpy.mock.calls[0][0]).toEqual(
expect.arrayContaining([{ $match: { activeMembersCount: { $gte: 2 } } }]),
);
});

it('returns current and comparison counts when comparison dates are given', async () => {
jest
.spyOn(Team, 'aggregate')
.mockResolvedValueOnce([{ totalTeams: 10 }])
.mockResolvedValueOnce([{ totalTeams: 8 }]);
const { getTotalActiveTeamCount } = overviewReportHelper();

const result = await getTotalActiveTeamCount(
'2026-10-04',
'2026-10-10',
'2026-09-27',
'2026-10-03',
);

expect(result.current).toBe(10);
expect(result.comparison).toBe(8);
expect(result).toHaveProperty('percentage');
});

it('returns 0 when no team qualifies', async () => {
jest.spyOn(Team, 'aggregate').mockResolvedValue([]);
const { getTotalActiveTeamCount } = overviewReportHelper();

const result = await getTotalActiveTeamCount('2026-10-04', '2026-10-10');

expect(result).toEqual({ current: 0 });
});
});
Loading