feat(teams): add Team entity and cutover Person.department to Person.teamId

Co-authored-by: Asis Ferrer <aferrer@aferrer.dev>
Co-committed-by: Asis Ferrer <aferrer@aferrer.dev>
This commit was merged in pull request #5.
This commit is contained in:
2026-06-26 00:29:09 +00:00
committed by Asis ferrer
parent cd38621f8b
commit 428dd0482d
52 changed files with 836 additions and 488 deletions
@@ -24,7 +24,7 @@ vi.mock("@/components/common/pageheader", () => ({
vi.mock("@/components/ui/button", () => ({
Button: ({ children }: { children: React.ReactNode }) =>
createElement("button", null, children),
createElement("button", { type: "button" }, children),
}))
vi.mock("next/link", () => ({
@@ -109,7 +109,9 @@ describe("asset detail page", () => {
)
const html = renderToStaticMarkup(
await AssetDetailPage({ params: Promise.resolve({ assetId: "asset-1" }) }),
await AssetDetailPage({
params: Promise.resolve({ assetId: "asset-1" }),
}),
)
expect(html).toContain("Asset Details")
+1 -1
View File
@@ -29,7 +29,7 @@ vi.mock("@/components/common/pagination", () => ({
vi.mock("@/components/ui/button", () => ({
Button: ({ children }: { children: React.ReactNode }) =>
createElement("button", null, children),
createElement("button", { type: "button" }, children),
}))
vi.mock("next/link", () => ({
@@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({
getI18n: vi.fn(),
findByIdWithUser: vi.fn(),
findById: vi.fn(),
listTeamsUseCase: vi.fn(),
personForm: vi.fn(),
push: vi.fn(),
toastError: vi.fn(),
@@ -27,6 +28,10 @@ vi.mock("@/services/person.service", () => ({
},
}))
vi.mock("@/use-cases/team.use-cases", () => ({
listTeamsUseCase: mocks.listTeamsUseCase,
}))
vi.mock("@/app/(dashboard)/people/_components/edit.person.form", () => ({
default: (props: unknown) => {
mocks.personForm(props)
@@ -54,8 +59,8 @@ const basePerson: PersonWithUser = {
id: "person-1",
firstName: "Ada",
lastName: "Lovelace",
department: "ENGINEERING",
teamId: null,
team: null,
email: "ada@example.test",
phone: "1234",
userId: null,
@@ -80,10 +85,16 @@ const personWithUser: PersonWithUser = {
},
}
const teams = [
{ id: "team-1", name: "Engineering" },
{ id: "team-2", name: "Sales" },
]
describe("edit person page wiring", () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.getI18n.mockResolvedValue({ dictionary: en, locale: "en" })
mocks.listTeamsUseCase.mockResolvedValue(teams)
})
it("loads the person without user, passes PersonWithoutUser to the edit form", async () => {
@@ -112,6 +123,7 @@ describe("edit person page wiring", () => {
...en.inventory.people.schema,
},
roleLabels: en.admin.users.roles,
teams,
}),
)
})
@@ -142,10 +154,7 @@ describe("edit person page wiring", () => {
}),
formCopy: es.admin.users.form,
roleLabels: es.admin.users.roles,
departmentCopy: es.inventory.people.departments,
fallbackCopy: expect.objectContaining({
unknownDepartment: es.inventory.people.fallback.unknownDepartment,
}),
teams,
}),
)
})
@@ -7,6 +7,7 @@ const mocks = vi.hoisted(() => ({
getI18n: vi.fn(),
findByIdWithUser: vi.fn(),
findById: vi.fn(),
listTeamsUseCase: vi.fn(),
redirect: vi.fn(),
personForm: vi.fn(),
}))
@@ -22,6 +23,10 @@ vi.mock("@/services/person.service", () => ({
},
}))
vi.mock("@/use-cases/team.use-cases", () => ({
listTeamsUseCase: mocks.listTeamsUseCase,
}))
vi.mock("next/navigation", () => ({
redirect: mocks.redirect,
useRouter: () => ({
@@ -49,10 +54,16 @@ vi.mock("sonner", () => ({
},
}))
const teams = [
{ id: "team-1", name: "Engineering" },
{ id: "team-2", name: "Sales" },
]
describe("person pages", () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" })
mocks.listTeamsUseCase.mockResolvedValue(teams)
})
it("renders the edit person page with Person heading and passes person to unified form", async () => {
@@ -66,7 +77,8 @@ describe("person pages", () => {
lastName: "Lovelace",
email: "ada@example.test",
phone: "1234",
department: "ENGINEERING",
teamId: "team-1",
team: { id: "team-1", name: "Engineering" },
userId: null,
isActive: true,
createdAt: new Date("2024-01-01"),
@@ -88,6 +100,7 @@ describe("person pages", () => {
firstName: "Ada",
lastName: "Lovelace",
}),
teams,
}),
)
})
+14 -8
View File
@@ -67,7 +67,8 @@ describe("person pages", () => {
lastName: "Lovelace",
email: "ada@example.test",
phone: "1234",
department: "ENGINEERING",
teamId: "team-1",
team: { id: "team-1", name: "Engineering" },
userId: null,
isActive: true,
createdAt: new Date("2024-01-01"),
@@ -87,7 +88,7 @@ describe("person pages", () => {
expect(html).toContain("Add Person")
// No username column — username header must not appear
expect(html).not.toContain("Username")
// No standalone username cell — only name, email, phone, department columns
// No standalone username cell — only name, email, phone, team columns
expect(html).not.toContain(">ada<")
// Name and other fields rendered
expect(html).toContain("Ada Lovelace")
@@ -110,7 +111,8 @@ describe("person pages", () => {
lastName: "Lovelace",
email: "ada@example.test",
phone: "1234",
department: "ENGINEERING",
teamId: "team-1",
team: { id: "team-1", name: "Engineering" },
userId: "user-1",
isActive: true,
createdAt: new Date("2024-01-01"),
@@ -137,7 +139,8 @@ describe("person pages", () => {
lastName: "Jones",
email: "bob@example.test",
phone: null,
department: "IT",
teamId: null,
team: null,
userId: null,
isActive: true,
createdAt: new Date("2024-01-01"),
@@ -193,7 +196,8 @@ describe("person pages", () => {
lastName: "Lovelace",
email: "ada@example.test",
phone: "1234",
department: "DRIVER",
teamId: "team-2",
team: { id: "team-2", name: "Driver" },
userId: null,
isActive: true,
createdAt: new Date("2024-01-01"),
@@ -221,7 +225,7 @@ describe("person pages", () => {
// Person detail fields
expect(html).toContain("Email")
expect(html).toContain("Phone")
expect(html).toContain("Department")
expect(html).toContain("Team")
expect(html).toContain("ada@example.test")
expect(html).toContain("Driver")
// Embedded assignments
@@ -239,7 +243,8 @@ describe("person pages", () => {
lastName: "Lovelace",
email: "ada@example.test",
phone: "1234",
department: "DRIVER",
teamId: null,
team: null,
userId: "user-1",
isActive: true,
createdAt: new Date("2024-01-01"),
@@ -287,7 +292,8 @@ describe("person pages", () => {
lastName: "Lovelace",
email: "ada@example.test",
phone: "1234",
department: "DRIVER",
teamId: null,
team: null,
userId: null,
isActive: true,
createdAt: new Date("2024-01-01"),
@@ -7,6 +7,7 @@ import { es } from "@/i18n/dictionaries/es"
const mocks = vi.hoisted(() => ({
createPersonUser: vi.fn(),
getI18n: vi.fn(),
listTeamsUseCase: vi.fn(),
push: vi.fn(),
toastError: vi.fn(),
toastSuccess: vi.fn(),
@@ -26,6 +27,10 @@ vi.mock("@/services/person.service", () => ({
},
}))
vi.mock("@/use-cases/team.use-cases", () => ({
listTeamsUseCase: mocks.listTeamsUseCase,
}))
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: mocks.push,
@@ -39,10 +44,16 @@ vi.mock("sonner", () => ({
},
}))
const teams = [
{ id: "team-1", name: "Engineering" },
{ id: "team-2", name: "Sales" },
]
describe("unified creation form page", () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" })
mocks.listTeamsUseCase.mockResolvedValue(teams)
})
it("renders unified form with Person fields, email, password, role, and NO_USER option in Spanish", async () => {
@@ -55,7 +66,7 @@ describe("unified creation form page", () => {
// Person fields
expect(html).toContain("Nombre")
expect(html).toContain("Apellido")
expect(html).toContain("Departamento")
expect(html).toContain("Equipo")
expect(html).toContain("Teléfono")
// User fields
@@ -86,7 +97,7 @@ describe("unified creation form page", () => {
// Person fields
expect(html).toContain("First Name")
expect(html).toContain("Last Name")
expect(html).toContain("Department")
expect(html).toContain("Team")
expect(html).toContain("Phone")
// User fields
@@ -108,18 +119,20 @@ describe("unified creation form page", () => {
// Person field placeholders
expect(html).toContain('placeholder="Nombre"') // firstNamePlaceholder (es)
expect(html).toContain('placeholder="Apellido"') // lastNamePlaceholder (es)
expect(html).toContain("Selecciona un departamento") // departmentPlaceholder
expect(html).toContain("Selecciona un equipo") // teamPlaceholder
expect(html).toContain('placeholder="Teléfono"') // phonePlaceholder (es)
})
it("renders department select with all PERSON_DEPARTMENTS values", async () => {
it("renders team select with active teams from listTeamsUseCase", async () => {
const { default: NewUserPage } = await import(
"@/app/(dashboard)/people/new/page"
)
const html = renderToStaticMarkup(await NewUserPage())
// Department values must use canonical enum values
expect(html).toContain('value="ADMINISTRATION"')
expect(html).toContain('value="team-1"')
expect(html).toContain("Engineering")
expect(html).toContain('value="team-2"')
expect(html).toContain("Sales")
})
})
+1 -49
View File
@@ -1,9 +1,6 @@
import { describe, expect, it } from "vitest"
import {
formatPersonDepartment,
formatUserRole,
} from "@/app/(dashboard)/people/_components/user.copy"
import { formatUserRole } from "@/app/(dashboard)/people/_components/user.copy"
describe("user copy helpers", () => {
const roleCopy = {
@@ -37,48 +34,3 @@ describe("user copy helpers", () => {
).toBe("Rol desconocido")
})
})
describe("formatPersonDepartment helper", () => {
const departmentCopy = {
IT: "IT",
ENGINEERING: "Ingeniería",
LOGISTICS: "Logística",
TRAFFIC: "Tráfico",
DRIVER: "Chofer",
ADMINISTRATION: "Administración",
SALES: "Ventas",
OTHER: "Otro",
}
const fallbackCopy = {
unknownDepartment: "Departamento desconocido",
unknownStatus: "Estado desconocido",
}
it("formats known department values with localized labels", () => {
expect(
formatPersonDepartment("ENGINEERING", departmentCopy, fallbackCopy),
).toBe("Ingeniería")
expect(
formatPersonDepartment("ADMINISTRATION", departmentCopy, fallbackCopy),
).toBe("Administración")
})
it("falls back for unknown department values", () => {
expect(
formatPersonDepartment("UNKNOWN_DEPT", departmentCopy, fallbackCopy),
).toBe("Departamento desconocido")
})
it("falls back for null department values", () => {
expect(formatPersonDepartment(null, departmentCopy, fallbackCopy)).toBe(
"Departamento desconocido",
)
})
it("falls back for undefined department values", () => {
expect(
formatPersonDepartment(undefined, departmentCopy, fallbackCopy),
).toBe("Departamento desconocido")
})
})