feat(ui): add team picker to person forms, list and detail pages

This commit is contained in:
2026-06-26 01:28:50 +02:00
parent 2919479f45
commit 43d8a133ca
8 changed files with 47 additions and 119 deletions
@@ -1,5 +1,6 @@
import { getI18n } from "@/i18n/server"
import { PersonService } from "@/services/person.service"
import { listTeamsUseCase } from "@/use-cases/team.use-cases"
import EditPersonForm from "../../_components/edit.person.form"
@@ -13,6 +14,7 @@ export default async function PersonEditPage({
const personCopy = dictionary.inventory.people
const userCopy = dictionary.admin.users
const person = await PersonService.findByIdWithUser(personId)
const teams = await listTeamsUseCase()
if (!person) {
return <div>{personCopy.edit.notFound}</div>
@@ -28,10 +30,8 @@ export default async function PersonEditPage({
formCopy={userCopy.form}
schemaCopy={{ ...userCopy.schema, ...personCopy.schema }}
roleLabels={userCopy.roles}
userFallbackCopy={userCopy.fallback}
departmentCopy={personCopy.departments}
fallbackCopy={personCopy.fallback}
submitButtonCopy={dictionary.common.submitButton}
teams={teams}
/>
</div>
)
+2 -11
View File
@@ -4,7 +4,6 @@ import { getI18n } from "@/i18n/server"
import { AssignmentService } from "@/services/assignment.service"
import { PersonService } from "@/services/person.service"
import { formatPersonDepartment } from "../_components/person.copy"
import {
formatUserRole,
type UserFallbackCopy,
@@ -45,16 +44,8 @@ export default async function PersonInfoPage({
<span>{person.phone}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">
{copy.detail.labels.department}
</span>
<span>
{formatPersonDepartment(
person.department,
copy.departments,
copy.fallback,
)}
</span>
<span className="text-gray-600">{copy.detail.labels.team}</span>
<span>{person.team?.name ?? copy.fallback.noTeam}</span>
</div>
{person.user ? (
<>
@@ -12,20 +12,16 @@ import {
type SubmitButtonCopy,
} from "@/components/forms/submitButton"
import { UserStatus } from "@/generated/prisma/client"
import { PERSON_DEPARTMENTS } from "@/lib/constants"
import {
buildUnifiedUpdateSchema,
type UnifiedSchemaCopy,
type UnifiedUpdateFormType,
} from "@/schemas/user.schema"
import type { PersonWithUser } from "@/services/person.service"
import type { TeamSummary } from "@/types"
import {
formatPersonDepartment,
formatUserRole,
type PersonDepartmentCopy,
type PersonFallbackCopy,
type UserFallbackCopy,
type UserFormCopy,
type UserRoleCopy,
} from "./user.copy"
@@ -35,19 +31,15 @@ export default function EditPersonForm({
formCopy,
schemaCopy,
roleLabels,
userFallbackCopy,
departmentCopy,
fallbackCopy,
submitButtonCopy,
teams,
}: {
person: PersonWithUser
formCopy: UserFormCopy
schemaCopy: UnifiedSchemaCopy
roleLabels: UserRoleCopy
userFallbackCopy: UserFallbackCopy
departmentCopy: PersonDepartmentCopy
fallbackCopy: PersonFallbackCopy
submitButtonCopy: SubmitButtonCopy
teams: TeamSummary[]
}) {
const router = useRouter()
const schema = useMemo(
@@ -68,7 +60,7 @@ export default function EditPersonForm({
id: person.id,
firstName: person.firstName,
lastName: person.lastName,
department: person.department ?? "OTHER",
teamId: person.teamId ?? null,
email: person.email ?? "",
phone: person.phone ?? "",
...(hasUser && user
@@ -116,12 +108,11 @@ export default function EditPersonForm({
placeholder={formCopy.lastNamePlaceholder}
register={register("lastName")}
/>
<DepartmentSelect
error={errors.department?.message}
<TeamSelect
error={errors.teamId?.message}
formCopy={formCopy}
departmentCopy={departmentCopy}
fallbackCopy={fallbackCopy}
register={register("department")}
register={register("teamId")}
teams={teams}
/>
<TextInput
error={errors.email?.message}
@@ -238,33 +229,31 @@ function RoleSelect({
)
}
function DepartmentSelect({
function TeamSelect({
error,
formCopy,
departmentCopy,
fallbackCopy,
register,
teams,
}: {
error?: string
formCopy: UserFormCopy
departmentCopy: PersonDepartmentCopy
fallbackCopy: PersonFallbackCopy
register: UseFormRegisterReturn
teams: TeamSummary[]
}) {
return (
<div className="flex flex-col gap-2">
<label htmlFor="department" className="mb-2 block text-lg">
{formCopy.departmentLabel}
<label htmlFor="teamId" className="mb-2 block text-lg">
{formCopy.teamLabel}
</label>
<select
id="department"
id="teamId"
{...register}
className={`w-full rounded-lg border px-4 py-2 ${error ? "border-error" : ""}`}
>
<option value="">{formCopy.departmentPlaceholder}</option>
{Object.keys(PERSON_DEPARTMENTS).map((department) => (
<option key={department} value={department}>
{formatPersonDepartment(department, departmentCopy, fallbackCopy)}
<option value="">{formCopy.teamPlaceholder}</option>
{teams.map((team) => (
<option key={team.id} value={team.id}>
{team.name}
</option>
))}
</select>
@@ -11,35 +11,27 @@ import {
SubmitButton,
type SubmitButtonCopy,
} from "@/components/forms/submitButton"
import { PERSON_DEPARTMENTS } from "@/lib/constants"
import {
buildUnifiedCreateSchema,
type UnifiedCreateFormType,
type UnifiedSchemaCopy,
} from "@/schemas/user.schema"
import type { TeamSummary } from "@/types"
import {
formatPersonDepartment,
type PersonDepartmentCopy,
type PersonFallbackCopy,
type UserFormCopy,
type UserRoleCopy,
} from "./user.copy"
import type { UserFormCopy, UserRoleCopy } from "./user.copy"
export default function NewUserForm({
formCopy,
schemaCopy,
roleLabels,
departmentCopy,
fallbackCopy,
submitButtonCopy,
teams,
}: {
formCopy: UserFormCopy
schemaCopy: UnifiedSchemaCopy
roleLabels: UserRoleCopy
departmentCopy: PersonDepartmentCopy
fallbackCopy: PersonFallbackCopy
submitButtonCopy: SubmitButtonCopy
teams: TeamSummary[]
}) {
const router = useRouter()
const schema = useMemo(
@@ -101,12 +93,11 @@ export default function NewUserForm({
placeholder={formCopy.lastNamePlaceholder}
register={register("lastName")}
/>
<DepartmentSelect
error={errors.department?.message}
<TeamSelect
error={errors.teamId?.message}
formCopy={formCopy}
departmentCopy={departmentCopy}
fallbackCopy={fallbackCopy}
register={register("department")}
register={register("teamId")}
teams={teams}
/>
<UserTextInput
error={errors.email?.message}
@@ -210,33 +201,31 @@ function RoleSelect({
)
}
function DepartmentSelect({
function TeamSelect({
error,
formCopy,
departmentCopy,
fallbackCopy,
register,
teams,
}: {
error?: string
formCopy: UserFormCopy
departmentCopy: PersonDepartmentCopy
fallbackCopy: PersonFallbackCopy
register: UseFormRegisterReturn
teams: TeamSummary[]
}) {
return (
<div className="flex flex-col gap-2">
<label htmlFor="department" className="mb-2 block text-lg">
{formCopy.departmentLabel}
<label htmlFor="teamId" className="mb-2 block text-lg">
{formCopy.teamLabel}
</label>
<select
id="department"
id="teamId"
{...register}
className={`w-full rounded-lg border px-4 py-2 ${error ? "border-error" : ""}`}
>
<option value="">{formCopy.departmentPlaceholder}</option>
{Object.keys(PERSON_DEPARTMENTS).map((department) => (
<option key={department} value={department}>
{formatPersonDepartment(department, departmentCopy, fallbackCopy)}
<option value="">{formCopy.teamPlaceholder}</option>
{teams.map((team) => (
<option key={team.id} value={team.id}>
{team.name}
</option>
))}
</select>
@@ -3,20 +3,4 @@ import type { Dictionary } from "@/i18n/dictionaries"
export type PersonListCopy = Dictionary["inventory"]["people"]["list"]
export type PersonDetailCopy = Dictionary["inventory"]["people"]["detail"]
export type PersonFormCopy = Dictionary["inventory"]["people"]["form"]
export type PersonDepartmentCopy =
Dictionary["inventory"]["people"]["departments"]
export type PersonFallbackCopy = Dictionary["inventory"]["people"]["fallback"]
export function formatPersonDepartment(
department: string | null | undefined,
departmentCopy: PersonDepartmentCopy,
fallbackCopy: PersonFallbackCopy,
) {
if (!department) {
return fallbackCopy.unknownDepartment
}
return department in departmentCopy
? departmentCopy[department as keyof PersonDepartmentCopy]
: fallbackCopy.unknownDepartment
}
@@ -6,8 +6,6 @@ export type UserStatusCopy = Dictionary["admin"]["users"]["status"]
export type UserFallbackCopy = Dictionary["admin"]["users"]["fallback"]
export type UserResetPasswordCopy =
Dictionary["admin"]["users"]["resetPassword"]
export type PersonDepartmentCopy =
Dictionary["inventory"]["people"]["departments"]
export type PersonFallbackCopy = Dictionary["inventory"]["people"]["fallback"]
export function formatUserRole(
@@ -19,17 +17,3 @@ export function formatUserRole(
? roleCopy[role as keyof UserRoleCopy]
: fallbackCopy.unknownRole
}
export function formatPersonDepartment(
department: string | null | undefined,
departmentCopy: PersonDepartmentCopy,
fallbackCopy: PersonFallbackCopy,
): string {
if (!department) {
return fallbackCopy.unknownDepartment
}
return department in departmentCopy
? departmentCopy[department as keyof PersonDepartmentCopy]
: fallbackCopy.unknownDepartment
}
+3 -2
View File
@@ -1,10 +1,12 @@
import { getI18n } from "@/i18n/server"
import { listTeamsUseCase } from "@/use-cases/team.use-cases"
import NewPersonForm from "../_components/new.person.form"
export default async function NewUserPage() {
const { dictionary } = await getI18n()
const copy = dictionary.admin.users
const teams = await listTeamsUseCase()
return (
<div className="flex flex-col gap-4">
@@ -15,9 +17,8 @@ export default async function NewUserPage() {
formCopy={copy.form}
schemaCopy={{ ...copy.schema, ...dictionary.inventory.people.schema }}
roleLabels={copy.roles}
departmentCopy={dictionary.inventory.people.departments}
fallbackCopy={dictionary.inventory.people.fallback}
submitButtonCopy={dictionary.common.submitButton}
teams={teams}
/>
</div>
)
+3 -13
View File
@@ -8,11 +8,6 @@ import { UserStatus } from "@/generated/prisma/client"
import { getI18n } from "@/i18n/server"
import { PersonService } from "@/services/person.service"
import {
formatPersonDepartment,
type PersonDepartmentCopy,
type PersonFallbackCopy,
} from "./_components/person.copy"
import TeamsTab from "./_components/teams.tab"
import {
formatUserRole,
@@ -55,8 +50,7 @@ export default async function PeoplePage(props: {
const userStatusCopy = userCopy.status
const userRoleLabels = userCopy.roles as UserRoleCopy
const userFallbackCopy = userCopy.fallback as UserFallbackCopy
const departmentCopy = copy.departments as PersonDepartmentCopy
const personFallbackCopy = copy.fallback as PersonFallbackCopy
const personFallbackCopy = copy.fallback
const peopleList = (
<div className="flex flex-col gap-4">
@@ -83,7 +77,7 @@ export default async function PeoplePage(props: {
{copy.list.columns.phone}
</th>
<th scope="col" className="p-4">
{copy.list.columns.department}
{copy.list.columns.team}
</th>
<th scope="col" className="p-4">
{copy.list.columns.role}
@@ -105,11 +99,7 @@ export default async function PeoplePage(props: {
<td className="p-4">{person.email}</td>
<td className="p-4">{person.phone}</td>
<td className="p-4">
{formatPersonDepartment(
person.department,
departmentCopy,
personFallbackCopy,
)}
{person.team?.name ?? personFallbackCopy.noTeam}
</td>
<td className="p-4">
{person.user