diff --git a/apps/api/src/tasks/tasks.service.spec.ts b/apps/api/src/tasks/tasks.service.spec.ts index 08623c2..de6b3a8 100644 --- a/apps/api/src/tasks/tasks.service.spec.ts +++ b/apps/api/src/tasks/tasks.service.spec.ts @@ -25,10 +25,10 @@ describe('TasksService', () => { await expect(service.findById('does-not-exist')).resolves.toBeNull(); }); - it('computes per-employee stats from the fixtures', () => { + it('computes per-employee stats from the fixtures', async () => { const employeeId = Assignments[0].employee_id; const expectedTotal = Assignments.filter(a => a.employee_id === employeeId).length; - const stats = service.getStatsForEmployee(employeeId); + const stats = await service.getStatsForEmployee(employeeId); expect(stats.totalTasks).toBe(expectedTotal); expect(stats.completionRate).toBeGreaterThanOrEqual(0); }); diff --git a/apps/api/src/tasks/tasks.service.ts b/apps/api/src/tasks/tasks.service.ts index 8b091af..0e71009 100644 --- a/apps/api/src/tasks/tasks.service.ts +++ b/apps/api/src/tasks/tasks.service.ts @@ -26,7 +26,15 @@ export class TasksService { return this.assignments.filter({ id }).first(); } - getStatsForEmployee(employeeId: string): ReturnType { + async getStatsForEmployee(employeeId: string): Promise> { + if (this.repo.enabled) { + // Same lookup math, hydrated from Postgres instead of the fixtures. + const [assignments, activities] = await Promise.all([ + this.repo.listAssignments(employeeId), + this.repo.listActivities(employeeId), + ]); + return new AssignmentLookup(assignments).getStats(employeeId, activities); + } return this.assignments.getStats(employeeId); } diff --git a/apps/api/src/users/users.service.spec.ts b/apps/api/src/users/users.service.spec.ts index 6ca41d8..483114f 100644 --- a/apps/api/src/users/users.service.spec.ts +++ b/apps/api/src/users/users.service.spec.ts @@ -24,8 +24,8 @@ describe('UsersService', () => { await expect(service.findById('does-not-exist')).resolves.toBeNull(); }); - it('computes stats over the shared fixtures', () => { - const stats = service.getStats(); + it('computes stats over the shared fixtures', async () => { + const stats = await service.getStats(); expect(stats.totalEmployees).toBe(Employees.length); const employeeRole = stats.roles.find(r => r.role === 'employee'); expect(employeeRole?.count).toBe(Employees.filter(e => e.role === 'employee').length); diff --git a/apps/api/src/users/users.service.ts b/apps/api/src/users/users.service.ts index cf0ae7b..b9e78dc 100644 --- a/apps/api/src/users/users.service.ts +++ b/apps/api/src/users/users.service.ts @@ -23,9 +23,11 @@ export class UsersService { return this.employees.getById(id); } - getStats(): ReturnType { - // Stats still come from the in-memory lookup util; Postgres path returns the - // same shape over the loaded fixture until a SQL aggregate lands. + async getStats(): Promise> { + if (this.repo.enabled) { + // Same lookup math, hydrated from Postgres instead of the fixtures. + return new EmployeeLookup(await this.repo.listEmployees()).getStats(); + } return this.employees.getStats(); } diff --git a/docs/handoffs/2026-07-26-api-postgres.md b/docs/handoffs/2026-07-26-api-postgres.md index 5fa27dc..374530e 100644 --- a/docs/handoffs/2026-07-26-api-postgres.md +++ b/docs/handoffs/2026-07-26-api-postgres.md @@ -73,6 +73,14 @@ the same fixtures the offline demo uses. - `EmployeeProfile.manager_id` is nullable; `Assignment.employee_id` / `source_id` are UUIDs. +## DB-backed stats (follow-up branch `feat/api-db-stats`) + +- `GET /users/stats` and `GET /tasks/stats/:employeeId` now hydrate the + `@worksight/common` lookup helpers from Postgres in DB mode, so stats + reflect live rows instead of always echoing fixtures. + `AssignmentLookup.getStats` gained an optional `activities` parameter + (defaults to the fixture set, so web callers are unaffected). + ## Surveys (follow-up branch `feat/api-surveys`) - Survey fixtures repaired: `SURVEY-123` → the real survey UUID, `EMP-001` → diff --git a/packages/common/src/utils/tasks.ts b/packages/common/src/utils/tasks.ts index 7b684fc..9c3fd90 100644 --- a/packages/common/src/utils/tasks.ts +++ b/packages/common/src/utils/tasks.ts @@ -48,8 +48,11 @@ export class AssignmentLookup extends BaseLookup { return this.filter({ employee_id, ...filters }); } - /** Compute stats for a given employee across tasks and activities */ - getStats(employee_id: string) { + /** + * Compute stats for a given employee across tasks and activities. + * @param activities activity set to aggregate; defaults to the fixture data + */ + getStats(employee_id: string, activities: Activity[] = Activities) { const employeeAssignments = this.filter({ employee_id }); const completedAssignments = employeeAssignments.filter({ status: 'completed' }); @@ -57,7 +60,7 @@ export class AssignmentLookup extends BaseLookup { const completedStoryPoints = completedAssignments.all().reduce((sum, t) => sum + (t.points ?? 0), 0); // Activities stats - const employeeActivities = Activities.filter((a) => a.employee_id === employee_id); + const employeeActivities = activities.filter((a) => a.employee_id === employee_id); const afterHours = employeeActivities.filter((a) => a.is_after_hours).length; const weekend = employeeActivities.filter((a) => a.is_weekend).length; const urgent = employeeActivities.filter((a) => a.is_urgent).length;