From d18c0cd4c1f3d13aac217ce949a42b05acd72328 Mon Sep 17 00:00:00 2001 From: kuyacarlo <106532351+kuyacarlo@users.noreply.github.com> Date: Sun, 26 Jul 2026 02:55:58 +0800 Subject: [PATCH] feat(api): serve /users/stats and /tasks/stats from Postgres in DB mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both stats endpoints always echoed the fixtures, so in DB mode they disagreed with the list endpoints they summarize. Hydrate the common lookup helpers from repository rows instead — same math, live data. AssignmentLookup.getStats previously hardcoded the fixture Activities import even when constructed with custom entries; it now takes the activity set as an optional parameter (defaulting to fixtures, so existing web callers are unaffected). Co-authored-by: Cursor --- apps/api/src/tasks/tasks.service.spec.ts | 4 ++-- apps/api/src/tasks/tasks.service.ts | 10 +++++++++- apps/api/src/users/users.service.spec.ts | 4 ++-- apps/api/src/users/users.service.ts | 8 +++++--- docs/handoffs/2026-07-26-api-postgres.md | 8 ++++++++ packages/common/src/utils/tasks.ts | 9 ++++++--- 6 files changed, 32 insertions(+), 11 deletions(-) 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;