Skip to content
Closed
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
12 changes: 6 additions & 6 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller.js';
import { AppService } from './app.service.js';
// import { AttendanceModule } from './attendance/attendance.module';
import { UsersModule } from './users/users.module.js';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TasksModule } from './tasks/tasks.module';
import { UsersModule } from './users/users.module';

@Module({
imports: [UsersModule],
imports: [UsersModule, TasksModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }
export class AppModule {}
37 changes: 37 additions & 0 deletions apps/api/src/tasks/tasks.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Controller, Get, NotFoundException, Param, Query } from '@nestjs/common';
import type { Activity, Assignment } from '@worksight/common';
import { TasksService } from './tasks.service';

@Controller('tasks')
export class TasksController {
constructor(private readonly tasksService: TasksService) {}

@Get()
getAll(@Query('employee_id') employeeId?: string): Assignment[] {
return this.tasksService.findAll(employeeId);
}

@Get('stats/:employeeId')
getStats(@Param('employeeId') employeeId: string) {
return this.tasksService.getStatsForEmployee(employeeId);
}

@Get(':id')
getOne(@Param('id') id: string): Assignment {
const assignment = this.tasksService.findById(id);
if (!assignment) {
throw new NotFoundException(`Task ${id} not found`);
}
return assignment;
}
}

@Controller('activities')
export class ActivitiesController {
constructor(private readonly tasksService: TasksService) {}

@Get()
getAll(@Query('employee_id') employeeId?: string): Activity[] {
return this.tasksService.findAllActivities(employeeId);
}
}
9 changes: 9 additions & 0 deletions apps/api/src/tasks/tasks.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { ActivitiesController, TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';

@Module({
controllers: [TasksController, ActivitiesController],
providers: [TasksService],
})
export class TasksModule {}
40 changes: 40 additions & 0 deletions apps/api/src/tasks/tasks.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Activities, Assignments } from '@worksight/common';
import { TasksService } from './tasks.service';

describe('TasksService', () => {
let service: TasksService;

beforeEach(() => {
service = new TasksService();
});

it('returns the shared assignment fixtures', () => {
expect(service.findAll()).toEqual(Assignments);
});

it('filters assignments by employee', () => {
const employeeId = Assignments[0].employee_id;
const expected = Assignments.filter(a => a.employee_id === employeeId);
expect(service.findAll(employeeId)).toEqual(expected);
});

it('finds an assignment by id', () => {
expect(service.findById(Assignments[0].id)).toEqual(Assignments[0]);
expect(service.findById('does-not-exist')).toBeNull();
});

it('computes per-employee stats from the fixtures', () => {
const employeeId = Assignments[0].employee_id;
const expectedTotal = Assignments.filter(a => a.employee_id === employeeId).length;
const stats = service.getStatsForEmployee(employeeId);
expect(stats.totalTasks).toBe(expectedTotal);
expect(stats.completionRate).toBeGreaterThanOrEqual(0);
});

it('returns the shared activity fixtures', () => {
expect(service.findAllActivities()).toEqual(Activities);
const employeeId = Activities[0].employee_id;
const expected = Activities.filter(a => a.employee_id === employeeId);
expect(service.findAllActivities(employeeId)).toEqual(expected);
});
});
30 changes: 30 additions & 0 deletions apps/api/src/tasks/tasks.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { Activity, ActivityLookup, Assignment, AssignmentLookup } from '@worksight/common';

@Injectable()
export class TasksService {
private readonly assignments = new AssignmentLookup();
private readonly activities = new ActivityLookup();

findAll(employeeId?: string): Assignment[] {
if (employeeId) {
return this.assignments.getAssignmentsByEmployee(employeeId).all();
}
return this.assignments.all();
}

findById(id: string): Assignment | null {
return this.assignments.filter({ id }).first();
}

getStatsForEmployee(employeeId: string): ReturnType<AssignmentLookup['getStats']> {
return this.assignments.getStats(employeeId);
}

findAllActivities(employeeId?: string): Activity[] {
if (employeeId) {
return this.activities.getActivitiesByEmployee(employeeId).all();
}
return this.activities.all();
}
}
46 changes: 38 additions & 8 deletions apps/api/src/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,46 @@
import { Controller, Get, Header, Param } from '@nestjs/common';
import { Roles } from '@worksight/common/types';
import { Controller, Get, NotFoundException, Param } from '@nestjs/common';
import type { EmployeeProfile, Team } from '@worksight/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}

@Get()
getAll(): EmployeeProfile[] {
return this.usersService.findAll();
}

@Get('stats')
getStats() {
return this.usersService.getStats();
}

@Get(':id')
getOne(@Param('id') id: string): EmployeeProfile {
const employee = this.usersService.findById(id);
if (!employee) {
throw new NotFoundException(`User ${id} not found`);
}
return employee;
}
}

@Controller('teams')
export class TeamsController {
constructor(private readonly usersService: UsersService) {}

@Get()
getAll() {
const role = Roles;
return { message: `Hello, NestJS!`, anotherMessage: `Hi, Karlo!`, test: "balls", role };
getAll(): Team[] {
return this.usersService.findAllTeams();
}

@Get(':id')
@Header('Content-Type', 'text/plain')
getOne(@Param('id') id: string) {
return `stuff ${id}`;
getOne(@Param('id') id: string): Team {
const team = this.usersService.findTeamById(id);
if (!team) {
throw new NotFoundException(`Team ${id} not found`);
}
return team;
}
}
6 changes: 3 additions & 3 deletions apps/api/src/users/users.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { TeamsController, UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
controllers: [UsersController],
controllers: [UsersController, TeamsController],
providers: [UsersService],
})
export class UsersModule { }
export class UsersModule {}
36 changes: 36 additions & 0 deletions apps/api/src/users/users.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Employees, Teams } from '@worksight/common';
import { UsersService } from './users.service';

describe('UsersService', () => {
let service: UsersService;

beforeEach(() => {
service = new UsersService();
});

it('returns the shared employee fixtures', () => {
expect(service.findAll()).toEqual(Employees);
expect(service.findAll().length).toBeGreaterThan(0);
});

it('finds an employee by id', () => {
const employee = Employees[0];
expect(service.findById(employee.id)).toEqual(employee);
});

it('returns null for an unknown employee id', () => {
expect(service.findById('does-not-exist')).toBeNull();
});

it('computes stats over the shared fixtures', () => {
const stats = 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);
});

it('returns the shared team fixtures', () => {
expect(service.findAllTeams()).toEqual(Teams);
expect(service.findTeamById(Teams[0].id)).toEqual(Teams[0]);
});
});
26 changes: 25 additions & 1 deletion apps/api/src/users/users.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
import { Injectable } from '@nestjs/common';
import { EmployeeLookup, EmployeeProfile, Team, TeamLookup, Teams } from '@worksight/common';

@Injectable()
export class UsersService { }
export class UsersService {
private readonly employees = new EmployeeLookup();
private readonly teams = new TeamLookup(Teams);

findAll(): EmployeeProfile[] {
return this.employees.all();
}

findById(id: string): EmployeeProfile | null {
return this.employees.getById(id);
}

getStats(): ReturnType<EmployeeLookup['getStats']> {
return this.employees.getStats();
}

findAllTeams(): Team[] {
return this.teams.all();
}

findTeamById(id: string): Team | null {
return this.teams.getById(id);
}
}
4 changes: 4 additions & 0 deletions apps/api/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*.spec.ts"]
}
11 changes: 7 additions & 4 deletions apps/api/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "es2022",
"moduleResolution": "bundler",
"module": "commonjs",
"moduleResolution": "node",
"noEmit": false,
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
Expand All @@ -20,8 +21,10 @@
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@worksight/common": ["../../packages/common/src"],
"@worksight/common/*": ["../../packages/common/src/*"]
// Resolve against the built package so type-check matches what Node
// loads at runtime (requires `pnpm --filter @worksight/common build`).
"@worksight/common": ["../../packages/common/dist"],
"@worksight/common/*": ["../../packages/common/dist/*"]
}
},
"include": ["src/**/*"],
Expand Down
61 changes: 50 additions & 11 deletions docs/handoffs/2026-07-25-wire-api-common.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,67 @@
# HANDOFF — Wire API to common (#17)

**Status:** Planned
**Branch:** `feat/mvp-wire-api`
**Status:** Done (MVP slice)
**Branch:** `feat/mvp-wire-api-17`
**Issue(s):** #17
**Last updated:** 2026-07-25

## Bottom line
Nest API returns/accepts shapes from `@worksight/common` types; at least one list endpoint serves common-shaped data.

Nest API returns/accepts shapes from `@worksight/common` types; users, teams,
tasks, and activities endpoints serve the shared fixtures.

## Current state
- `users.controller.ts` imports `Roles` from `@worksight/common/types`
- common package linked in api `package.json`

- `@worksight/common` is a `workspace:*` dependency of the API; the api tsconfig
resolves it against the built `packages/common/dist` so type-check matches
what Node loads at runtime (build common first).
- `apps/api` now emits real output: the api tsconfig previously inherited
`noEmit: true` from the root, so `nest build` produced an empty `dist/`. Fixed
with `noEmit: false` + CommonJS emit; added `tsconfig.build.json` so spec
files stay out of `dist/`.
- `packages/common` build now runs `tsc-alias --resolve-full-paths`; the
previous dist was unloadable ESM (`export * from './data'` directory imports).
- Fixed `BaseLookup.filter()` in common: it reconstructed subclasses with
`new Cls(schema, entries)` while subclasses take `(entries)`, so every chained
filter/`getById` silently returned garbage.
- Endpoints (all fixture-backed, typed from common):
- `GET /users` → `EmployeeProfile[]`, `GET /users/:id` (404 if missing),
`GET /users/stats`
- `GET /teams` → `Team[]`, `GET /teams/:id`
- `GET /tasks` → `Assignment[]` (`?employee_id=` filter), `GET /tasks/:id`,
`GET /tasks/stats/:employeeId`
- `GET /activities` → `Activity[]` (`?employee_id=` filter)
- Unit tests for `UsersService` / `TasksService` assert responses equal the
shared fixtures.

## Hook points
- `apps/api/src/**` controllers/services/DTOs
- Map DB/Supabase rows → common types if needed

- `apps/api/src/users/**`, `apps/api/src/tasks/**`
- Swap the `*Lookup` fixture sources for DB/Supabase rows mapped to common types
when real persistence lands.

## How to verify

```bash
pnpm --filter @worksight/common build # required before api type-check/build
pnpm --filter @worksight/api type-check
pnpm --filter @worksight/api start:dev
# curl list endpoint; shape matches common types
pnpm --filter @worksight/api build
pnpm --filter @worksight/api test
pnpm --filter @worksight/api lint
PORT=3123 node apps/api/dist/main.js &
curl localhost:3123/users # shape matches EmployeeProfile[]
curl localhost:3123/tasks # shape matches Assignment[]
```

## Done means
- [ ] Shared types on request/response path
- [ ] Type-check green

- [x] Shared types on request/response path
- [x] Type-check green

## Known gaps

- Responses are fixture-backed only; no DB/Supabase reads yet.
- Attendance / survey / burnout types exist in common but have no API endpoints
yet (attendance module is still commented out in `app.module.ts`).
- Employee fixture ids in common are not all valid UUIDs, so responses fail
strict `EmployeeProfileSchema.parse` even though the TS shapes match.
Loading
Loading