feat(teams): add Team entity and replace PersonDepartment with teamId #6

Merged
aferrer merged 11 commits from feature/add-teams into dev 2026-06-26 00:32:38 +00:00
Owner

Summary

Replaces the PersonDepartment enum with a user-managed Team entity. Teams are created, renamed, and deleted from /people?tab=teams (ADMIN-gated). Person.department becomes Person.teamId (nullable FK, onDelete: SetNull). Migration backfills Person.teamId from the old enum and drops the enum and column in one transaction.

What's in

  • New Team Prisma model (id, name, createdAt, updatedAt) with a case-insensitive unique index on lower("name").
  • Person.teamId FK (nullable, onDelete: SetNull).
  • Migration that:
    1. Creates the Team table and the case-insensitive unique index.
    2. Seeds 8 teams with English display names (IT, Engineering, Logistics, Traffic, Driver, Administration, Sales, Other).
    3. Backfills Person.teamId from the old PersonDepartment enum.
    4. Drops the Person.department column, the old index, and the PersonDepartment enum.
  • Full layer: src/schemas/team.schema.ts, src/services/team.service.ts, src/use-cases/team.use-cases.ts, src/actions/team.actions.ts, src/types/team.ts.
  • Tab UI on /people?tab=teams with TeamsTab, TeamListTable, TeamCreateForm, TeamEditForm.
  • Person forms (new + edit) use a team picker instead of the old department select.
  • Person list cell shows person.team?.name (with fallback).
  • Person detail view shows team name (with fallback).
  • i18n in both en.ts and es.ts: new inventory.teams.* keys, removed inventory.people.departments, added inventory.people.* team keys.
  • Import flow updated: department dropdown replaced with team picker.
  • Tests: unit (schema, i18n), integration (use-cases), e2e (people flow).

Key decisions

  • Hard delete on Team (no deletedAt). FK onDelete: SetNull handles cascade automatically.
  • Case-insensitive unique name via raw SQL: CREATE UNIQUE INDEX team_name_lower_unique ON "Team" (lower("name")). Prisma's partial unique indexes don't support functional expressions in this version.
  • ADMIN gate at the route layout (src/app/(dashboard)/people/layout.tsx). The team picker is open to any authenticated user (needed for the person form).
  • Empty string in team picker is accepted as "no team" via z.union([z.string().uuid(), z.literal("")]).transform((val) => val === "" ? null : val).

Verification

  • bun run test:unit — 223 tests
  • bun run test:integration — 86 tests
  • bun run test:e2e — 4 people e2e tests pass (tab routing, team CRUD, person with/without team)
  • bunx tsc --noEmit
  • bunx prisma validate
  • bunx biome check .

PR chain (already merged)

  • Tracker: feature/add-teams
  • PR #4: Slice 1 (Team management foundation) — closed as no-op (tracker and Slice 1 were identical)
  • PR #5: Slice 2 (Person-team cutover) — merged into tracker with squash

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

Deployment notes

  • The migration is destructive (drops the PersonDepartment enum and Person.department column). Run bunx prisma migrate deploy and verify the seed teams and backfilled Person.teamId values before declaring the deploy successful.
  • After the deploy, the old /admin/users route is fully replaced by /people. No redirect is needed because the route was already migrated to /people in a previous change.
## Summary Replaces the `PersonDepartment` enum with a user-managed `Team` entity. Teams are created, renamed, and deleted from `/people?tab=teams` (ADMIN-gated). `Person.department` becomes `Person.teamId` (nullable FK, `onDelete: SetNull`). Migration backfills `Person.teamId` from the old enum and drops the enum and column in one transaction. ## What's in - New `Team` Prisma model (id, name, createdAt, updatedAt) with a case-insensitive unique index on `lower("name")`. - `Person.teamId` FK (nullable, `onDelete: SetNull`). - Migration that: 1. Creates the `Team` table and the case-insensitive unique index. 2. Seeds 8 teams with English display names (IT, Engineering, Logistics, Traffic, Driver, Administration, Sales, Other). 3. Backfills `Person.teamId` from the old `PersonDepartment` enum. 4. Drops the `Person.department` column, the old index, and the `PersonDepartment` enum. - Full layer: `src/schemas/team.schema.ts`, `src/services/team.service.ts`, `src/use-cases/team.use-cases.ts`, `src/actions/team.actions.ts`, `src/types/team.ts`. - Tab UI on `/people?tab=teams` with `TeamsTab`, `TeamListTable`, `TeamCreateForm`, `TeamEditForm`. - Person forms (new + edit) use a team picker instead of the old department select. - Person list cell shows `person.team?.name` (with `—` fallback). - Person detail view shows team name (with `—` fallback). - i18n in both `en.ts` and `es.ts`: new `inventory.teams.*` keys, removed `inventory.people.departments`, added `inventory.people.*` team keys. - Import flow updated: department dropdown replaced with team picker. - Tests: unit (schema, i18n), integration (use-cases), e2e (people flow). ## Key decisions - **Hard delete** on Team (no `deletedAt`). FK `onDelete: SetNull` handles cascade automatically. - **Case-insensitive unique name** via raw SQL: `CREATE UNIQUE INDEX team_name_lower_unique ON "Team" (lower("name"))`. Prisma's partial unique indexes don't support functional expressions in this version. - **ADMIN gate** at the route layout (`src/app/(dashboard)/people/layout.tsx`). The team picker is open to any authenticated user (needed for the person form). - **Empty string in team picker** is accepted as "no team" via `z.union([z.string().uuid(), z.literal("")]).transform((val) => val === "" ? null : val)`. ## Verification - ✅ `bun run test:unit` — 223 tests - ✅ `bun run test:integration` — 86 tests - ✅ `bun run test:e2e` — 4 people e2e tests pass (tab routing, team CRUD, person with/without team) - ✅ `bunx tsc --noEmit` - ✅ `bunx prisma validate` - ✅ `bunx biome check .` ## PR chain (already merged) - Tracker: `feature/add-teams` - PR #4: Slice 1 (Team management foundation) — closed as no-op (tracker and Slice 1 were identical) - PR #5: Slice 2 (Person-team cutover) — merged into tracker with squash ## 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` ## Deployment notes - The migration is destructive (drops the `PersonDepartment` enum and `Person.department` column). Run `bunx prisma migrate deploy` and verify the seed teams and backfilled `Person.teamId` values before declaring the deploy successful. - After the deploy, the old `/admin/users` route is fully replaced by `/people`. No redirect is needed because the route was already migrated to `/people` in a previous change.
aferrer added 11 commits 2026-06-26 00:29:43 +00:00
aferrer merged commit efda051aa3 into dev 2026-06-26 00:32:38 +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#6