feat(people): cutover Person from department to teamId (Slice 2/2) #5

Merged
aferrer merged 12 commits from feature/add-teams-slice-2 into feature/add-teams 2026-06-26 00:29:09 +00:00
Owner

Summary

Cuts over Person.department to Person.teamId, drops the PersonDepartment enum, and cleans up the legacy i18n keys. This is Slice 2 of 2 in the add-teams chained PR. Depends on Slice 1 (#4), which added the Team entity and management tab.

What's in

  • Person.department replaced with Person.teamId (FK to Team, nullable, onDelete: SetNull).
  • Migration that backfills Person.teamId from the old PersonDepartment enum (using the 8 seed teams from Slice 1's migration), then drops the Person.department column, the @@index([department, deletedAt]) index, and the PersonDepartment enum — all in one transaction.
  • person.schema.ts and user.schema.ts updated: teamId replaces department. Empty string from the "Select a team" option is accepted and transformed to null via z.union + transform.
  • PersonService read projections now include team: { id, name } so the person list and detail pages can render person.team?.name.
  • person.use-cases.ts updated: createPersonUseCase, updatePersonUseCase, createPersonUserUseCase, updatePersonUserUseCase now handle teamId (preflight check against existing teams, teamNotFound mapping).
  • New team picker in new.person.form.tsx and edit.person.form.tsx (replaces the old department select).
  • Person list cell shows person.team?.name (with fallback when null).
  • Person detail view shows team name (with fallback when null).
  • i18n: inventory.people.departments removed from en.ts and es.ts. New keys: inventory.people.list.columns.team, inventory.people.detail.labels.team, inventory.people.form.{teamLabel,teamPlaceholder}, inventory.people.fallback.noTeam, inventory.people.schema.teamIdInvalid, inventory.people.actions.teamNotFound, admin.users.form.{teamLabel,teamPlaceholder}.
  • Import flow (import.form.tsx + import.actions.ts) updated: department dropdown replaced with team picker; unknown department maps to null (no team).
  • Tests updated: person.use-cases.test.ts, person-user.use-cases.test.ts, update-person-user.use-cases.test.ts, person.schema.test.ts, unified-create.schema.test.ts, unified-update.schema.test.ts, core-schemas.test.ts, i18n dictionary tests, and helpers updated to use teamId.
  • New e2e spec tests/e2e/people.spec.ts covering: tab routing via URL, team CRUD, person with team (shows in list and detail), person without team (shows fallback).
  • Pre-existing biome lint errors fixed across the codebase (non-null assertions, missing type="button" in mocked Button components).

What's out

Nothing — this is the final slice of the add-teams change.

Hard delete decision

Team uses hard delete (no deletedAt). FK onDelete: SetNull on Person.teamId handles the cascade automatically.

Empty string in team picker

The "Select a team" option sends an empty string. The Zod schema uses z.union([z.string().uuid(), z.literal("")]).transform((val) => val === "" ? null : val) to accept it as "no team". This keeps the form's input type as string (not unknown) so react-hook-form's zodResolver type inference stays correct.

PR chain

Verification

  • bun run test:unit — 223 tests
  • bun run test:integration — 86 tests
  • bun run test:e2e — 4 people e2e tests pass
  • bunx tsc --noEmit
  • bunx prisma validate
  • bunx biome check .

Diff: ~22 commits on top of Slice 1.

Related

  • Proposal: openspec/changes/add-teams/proposal.md
  • Specs: openspec/changes/add-teams/specs/team-management.md, openspec/changes/add-teams/specs/person-team.md
  • Design: openspec/changes/add-teams/design.md
  • Tasks: openspec/changes/add-teams/tasks.md
  • Slice 1 PR: feat(teams): add Team entity foundation (Slice 1/2) (#4)
## Summary Cuts over `Person.department` to `Person.teamId`, drops the `PersonDepartment` enum, and cleans up the legacy i18n keys. This is **Slice 2 of 2** in the `add-teams` chained PR. Depends on Slice 1 (#4), which added the `Team` entity and management tab. ## What's in - `Person.department` replaced with `Person.teamId` (FK to `Team`, nullable, `onDelete: SetNull`). - Migration that backfills `Person.teamId` from the old `PersonDepartment` enum (using the 8 seed teams from Slice 1's migration), then drops the `Person.department` column, the `@@index([department, deletedAt])` index, and the `PersonDepartment` enum — all in one transaction. - `person.schema.ts` and `user.schema.ts` updated: `teamId` replaces `department`. Empty string from the "Select a team" option is accepted and transformed to `null` via `z.union + transform`. - `PersonService` read projections now include `team: { id, name }` so the person list and detail pages can render `person.team?.name`. - `person.use-cases.ts` updated: `createPersonUseCase`, `updatePersonUseCase`, `createPersonUserUseCase`, `updatePersonUserUseCase` now handle `teamId` (preflight check against existing teams, `teamNotFound` mapping). - New team picker in `new.person.form.tsx` and `edit.person.form.tsx` (replaces the old department select). - Person list cell shows `person.team?.name` (with `—` fallback when null). - Person detail view shows team name (with `—` fallback when null). - i18n: `inventory.people.departments` removed from `en.ts` and `es.ts`. New keys: `inventory.people.list.columns.team`, `inventory.people.detail.labels.team`, `inventory.people.form.{teamLabel,teamPlaceholder}`, `inventory.people.fallback.noTeam`, `inventory.people.schema.teamIdInvalid`, `inventory.people.actions.teamNotFound`, `admin.users.form.{teamLabel,teamPlaceholder}`. - Import flow (`import.form.tsx` + `import.actions.ts`) updated: department dropdown replaced with team picker; unknown department maps to `null` (no team). - Tests updated: `person.use-cases.test.ts`, `person-user.use-cases.test.ts`, `update-person-user.use-cases.test.ts`, `person.schema.test.ts`, `unified-create.schema.test.ts`, `unified-update.schema.test.ts`, `core-schemas.test.ts`, i18n dictionary tests, and helpers updated to use `teamId`. - New e2e spec `tests/e2e/people.spec.ts` covering: tab routing via URL, team CRUD, person with team (shows in list and detail), person without team (shows `—` fallback). - Pre-existing biome lint errors fixed across the codebase (non-null assertions, missing `type="button"` in mocked Button components). ## What's out Nothing — this is the final slice of the add-teams change. ## Hard delete decision Team uses **hard delete** (no `deletedAt`). FK `onDelete: SetNull` on `Person.teamId` handles the cascade automatically. ## Empty string in team picker The "Select a team" option sends an empty string. The Zod schema uses `z.union([z.string().uuid(), z.literal("")]).transform((val) => val === "" ? null : val)` to accept it as "no team". This keeps the form's input type as `string` (not `unknown`) so react-hook-form's `zodResolver` type inference stays correct. ## PR chain - Tracker: `feature/add-teams` - Slice 1 (this PR's base): `feature/add-teams-slice-1` — PR #4 - This PR: `feature/add-teams-slice-2` ## Verification - ✅ `bun run test:unit` — 223 tests - ✅ `bun run test:integration` — 86 tests - ✅ `bun run test:e2e` — 4 people e2e tests pass - ✅ `bunx tsc --noEmit` - ✅ `bunx prisma validate` - ✅ `bunx biome check .` Diff: ~22 commits on top of Slice 1. ## Related - Proposal: `openspec/changes/add-teams/proposal.md` - Specs: `openspec/changes/add-teams/specs/team-management.md`, `openspec/changes/add-teams/specs/person-team.md` - Design: `openspec/changes/add-teams/design.md` - Tasks: `openspec/changes/add-teams/tasks.md` - Slice 1 PR: #4
aferrer changed target branch from feature/add-teams-slice-1 to feature/add-teams 2026-06-26 00:27:57 +00:00
aferrer added 12 commits 2026-06-26 00:27:57 +00:00
aferrer merged commit 428dd0482d into feature/add-teams 2026-06-26 00:29:09 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: aferrer/stock-manager#5