refactor: rename recipients route to people, update all frontend references

This commit is contained in:
2026-06-16 11:26:21 +02:00
parent d67f31cf54
commit ecc3cf1b55
37 changed files with 553 additions and 194 deletions
+2 -2
View File
@@ -123,7 +123,7 @@ export async function importItems(formData: ImportFormType) {
file: [ file: [
"Only one of category or categoryId is allowed, you must select one of them", "Only one of category or categoryId is allowed, you must select one of them",
], ],
} },
} }
} }
@@ -342,4 +342,4 @@ export async function importItems(formData: ImportFormType) {
success: true, success: true,
message: "Items imported successfully!", message: "Items imported successfully!",
} }
} }
+1 -1
View File
@@ -94,4 +94,4 @@ export async function updatePerson(formData: UpdatePersonFormType) {
message: copy.actions.updateFailure, message: copy.actions.updateFailure,
} }
} }
} }
+1 -1
View File
@@ -35,4 +35,4 @@ export function localizePersonFieldErrors(
messages.map((message) => localizePersonMessage(message, copy)), messages.map((message) => localizePersonMessage(message, copy)),
]), ]),
) )
} }
+6 -6
View File
@@ -1,7 +1,7 @@
import { getI18n } from "@/i18n/server" import { getI18n } from "@/i18n/server"
import { AssetService } from "@/services/asset.service" import { AssetService } from "@/services/asset.service"
import { ItemService } from "@/services/item.service" import { ItemService } from "@/services/item.service"
import { RecipientService } from "@/services/recipient.service" import { PersonService } from "@/services/person.service"
import Card from "./_components/card" import Card from "./_components/card"
@@ -10,7 +10,7 @@ export default async function Home() {
const copy = dictionary.dashboardHome const copy = dictionary.dashboardHome
const totalItems = await ItemService.findAllItemsCount() const totalItems = await ItemService.findAllItemsCount()
const totalAssets = await AssetService.findAllAssetsCount() const totalAssets = await AssetService.findAllAssetsCount()
const totalRecipients = await RecipientService.findAllRecipientsCount() const totalPeople = await PersonService.findAllPeopleCount()
return ( return (
<div className="container mx-auto p-4"> <div className="container mx-auto p-4">
@@ -65,10 +65,10 @@ export default async function Home() {
} }
/> />
<Card <Card
title={copy.cards.recipients.title} title={copy.cards.people.title}
total={totalRecipients} total={totalPeople}
countLabel={copy.cards.recipients.countLabel} countLabel={copy.cards.people.countLabel}
href="/recipients" href="/people"
icon={ icon={
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -3,7 +3,7 @@ import type { UpdateAssignmentFormType } from "@/schemas/assignment.schema"
import { AssetService } from "@/services/asset.service" import { AssetService } from "@/services/asset.service"
import { AssignmentService } from "@/services/assignment.service" import { AssignmentService } from "@/services/assignment.service"
import { ItemService } from "@/services/item.service" import { ItemService } from "@/services/item.service"
import { RecipientService } from "@/services/recipient.service" import { PersonService } from "@/services/person.service"
import type { Item } from "@/types" import type { Item } from "@/types"
import AssignmentForm from "../../_components/edit.assignment.form" import AssignmentForm from "../../_components/edit.assignment.form"
@@ -14,7 +14,7 @@ export default async function EditAssignmentPage({
}) { }) {
const { assignmentId } = await params const { assignmentId } = await params
const assignment = await AssignmentService.findById(assignmentId) const assignment = await AssignmentService.findById(assignmentId)
const recipients = await RecipientService.findAll() const people = await PersonService.findAll()
const items = await ItemService.findAllWithStock() const items = await ItemService.findAllWithStock()
const assets = await AssetService.findAll() const assets = await AssetService.findAll()
const { dictionary } = await getI18n() const { dictionary } = await getI18n()
@@ -37,7 +37,7 @@ export default async function EditAssignmentPage({
<h1 className="text-2xl font-bold">{copy.edit.title}</h1> <h1 className="text-2xl font-bold">{copy.edit.title}</h1>
</div> </div>
<AssignmentForm <AssignmentForm
recipients={recipients} recipients={people}
items={items} items={items}
assets={assets} assets={assets}
initialData={assignment as UpdateAssignmentFormType} initialData={assignment as UpdateAssignmentFormType}
@@ -15,13 +15,13 @@ import {
buildUpdateAssignmentSchema, buildUpdateAssignmentSchema,
type UpdateAssignmentFormType, type UpdateAssignmentFormType,
} from "@/schemas/assignment.schema" } from "@/schemas/assignment.schema"
import type { Asset, Item, Recipient } from "@/types" import type { Asset, Item, Person } from "@/types"
type AssignmentFormCopy = Dictionary["inventory"]["assignments"]["form"] type AssignmentFormCopy = Dictionary["inventory"]["assignments"]["form"]
type AssignmentSchemaCopy = Dictionary["inventory"]["assignments"]["schema"] type AssignmentSchemaCopy = Dictionary["inventory"]["assignments"]["schema"]
interface Props { interface Props {
recipients: Recipient[] recipients: Person[]
items: Item[] items: Item[]
assets: Asset[] assets: Asset[]
initialData: UpdateAssignmentFormType initialData: UpdateAssignmentFormType
@@ -15,13 +15,13 @@ import {
buildCreateAssignmentSchema, buildCreateAssignmentSchema,
type CreateAssignmentFormType, type CreateAssignmentFormType,
} from "@/schemas/assignment.schema" } from "@/schemas/assignment.schema"
import type { Asset, Item, Recipient } from "@/types" import type { Asset, Item, Person } from "@/types"
type AssignmentFormCopy = Dictionary["inventory"]["assignments"]["form"] type AssignmentFormCopy = Dictionary["inventory"]["assignments"]["form"]
type AssignmentSchemaCopy = Dictionary["inventory"]["assignments"]["schema"] type AssignmentSchemaCopy = Dictionary["inventory"]["assignments"]["schema"]
interface Props { interface Props {
recipients: Recipient[] recipients: Person[]
items: Item[] items: Item[]
assets: Asset[] assets: Asset[]
formCopy: AssignmentFormCopy formCopy: AssignmentFormCopy
+3 -3
View File
@@ -1,12 +1,12 @@
import { getI18n } from "@/i18n/server" import { getI18n } from "@/i18n/server"
import { AssetService } from "@/services/asset.service" import { AssetService } from "@/services/asset.service"
import { ItemService } from "@/services/item.service" import { ItemService } from "@/services/item.service"
import { RecipientService } from "@/services/recipient.service" import { PersonService } from "@/services/person.service"
import AssignmentForm from "../_components/new.assignment.form" import AssignmentForm from "../_components/new.assignment.form"
export default async function NewAssignmentPage() { export default async function NewAssignmentPage() {
const recipients = await RecipientService.findAll() const people = await PersonService.findAll()
const items = await ItemService.findAllWithStock() const items = await ItemService.findAllWithStock()
const assets = await AssetService.findAllAvailable() const assets = await AssetService.findAllAvailable()
const { dictionary } = await getI18n() const { dictionary } = await getI18n()
@@ -18,7 +18,7 @@ export default async function NewAssignmentPage() {
<h1 className="text-2xl font-bold">{copy.new.title}</h1> <h1 className="text-2xl font-bold">{copy.new.title}</h1>
</div> </div>
<AssignmentForm <AssignmentForm
recipients={recipients} recipients={people}
items={items} items={items}
assets={assets} assets={assets}
formCopy={copy.form} formCopy={copy.form}
+1 -1
View File
@@ -63,7 +63,7 @@ export default async function AssignmentsPage(props: {
<tr key={assignment.id} className="border-b"> <tr key={assignment.id} className="border-b">
<td className="p-4"> <td className="p-4">
<Link <Link
href={`/recipients/${assignment?.recipient?.id}`} href={`/people/${assignment?.recipient?.id}`}
className="hover:underline" className="hover:underline"
> >
{assignment?.recipient?.firstName}{" "} {assignment?.recipient?.firstName}{" "}
@@ -3,7 +3,7 @@
import { getI18n } from "@/i18n/server" import { getI18n } from "@/i18n/server"
import { AssetService } from "@/services/asset.service" import { AssetService } from "@/services/asset.service"
import { ItemService } from "@/services/item.service" import { ItemService } from "@/services/item.service"
import { RecipientService } from "@/services/recipient.service" import { PersonService } from "@/services/person.service"
import type { AssetWithAssignment } from "@/types" import type { AssetWithAssignment } from "@/types"
import EditAssetForm from "../../_components/edit.asset.form" import EditAssetForm from "../../_components/edit.asset.form"
@@ -15,7 +15,7 @@ export default async function EditAssetPage({
}) { }) {
const { assetId } = await params const { assetId } = await params
const items = await ItemService.findAll() const items = await ItemService.findAll()
const recipients = await RecipientService.findAll() const people = await PersonService.findAll()
const asset = await AssetService.findById(assetId) const asset = await AssetService.findById(assetId)
const { dictionary } = await getI18n() const { dictionary } = await getI18n()
const copy = dictionary.inventory.assets const copy = dictionary.inventory.assets
@@ -31,7 +31,7 @@ export default async function EditAssetPage({
</div> </div>
<EditAssetForm <EditAssetForm
items={items} items={items}
recipients={recipients} recipients={people}
asset={asset as unknown as AssetWithAssignment} asset={asset as unknown as AssetWithAssignment}
formCopy={copy.form} formCopy={copy.form}
schemaCopy={copy.schema} schemaCopy={copy.schema}
@@ -18,7 +18,7 @@ import {
import type { import type {
AssetWithAssignment, AssetWithAssignment,
Item, Item,
Recipient, Person,
UpdateAssetStatus, UpdateAssetStatus,
} from "@/types" } from "@/types"
@@ -31,7 +31,7 @@ import type {
interface EditAssetFormProps { interface EditAssetFormProps {
asset: AssetWithAssignment asset: AssetWithAssignment
items: Item[] items: Item[]
recipients: Recipient[] recipients: Person[]
formCopy: AssetFormCopy formCopy: AssetFormCopy
schemaCopy: AssetSchemaCopy schemaCopy: AssetSchemaCopy
statusCopy: AssetStatusCopy statusCopy: AssetStatusCopy
@@ -15,7 +15,7 @@ import {
buildCreateAssetSchema, buildCreateAssetSchema,
type CreateAssetFormType, type CreateAssetFormType,
} from "@/schemas/asset.schema" } from "@/schemas/asset.schema"
import type { ItemWithoutStock, Recipient } from "@/types" import type { ItemWithoutStock, Person } from "@/types"
import type { import type {
AssetFormCopy, AssetFormCopy,
@@ -25,7 +25,7 @@ import type {
interface NewAssetFormProps { interface NewAssetFormProps {
items: ItemWithoutStock[] items: ItemWithoutStock[]
recipients: Recipient[] recipients: Person[]
formCopy: AssetFormCopy formCopy: AssetFormCopy
schemaCopy: AssetSchemaCopy schemaCopy: AssetSchemaCopy
statusCopy: AssetStatusCopy statusCopy: AssetStatusCopy
@@ -2,13 +2,13 @@
import { getI18n } from "@/i18n/server" import { getI18n } from "@/i18n/server"
import { ItemService } from "@/services/item.service" import { ItemService } from "@/services/item.service"
import { RecipientService } from "@/services/recipient.service" import { PersonService } from "@/services/person.service"
import NewAssetForm from "../_components/new.asset.form" import NewAssetForm from "../_components/new.asset.form"
export default async function NewAssetPage() { export default async function NewAssetPage() {
const items = await ItemService.findAllAssignable() const items = await ItemService.findAllAssignable()
const recipients = await RecipientService.findAll() const people = await PersonService.findAll()
const { dictionary } = await getI18n() const { dictionary } = await getI18n()
const copy = dictionary.inventory.assets const copy = dictionary.inventory.assets
@@ -19,7 +19,7 @@ export default async function NewAssetPage() {
</div> </div>
<NewAssetForm <NewAssetForm
items={items} items={items}
recipients={recipients} recipients={people}
formCopy={copy.form} formCopy={copy.form}
schemaCopy={copy.schema} schemaCopy={copy.schema}
statusCopy={copy.status} statusCopy={copy.status}
@@ -1,19 +1,19 @@
import { getI18n } from "@/i18n/server" import { getI18n } from "@/i18n/server"
import { RecipientService } from "@/services/recipient.service" import { PersonService } from "@/services/person.service"
import RecipientForm from "../../_components/recipient.form" import PersonForm from "../../_components/person.form"
export default async function RecipientEditPage({ export default async function PersonEditPage({
params, params,
}: { }: {
params: Promise<{ recipientId: string }> params: Promise<{ personId: string }>
}) { }) {
const { recipientId } = await params const { personId } = await params
const { dictionary } = await getI18n() const { dictionary } = await getI18n()
const copy = dictionary.inventory.recipients const copy = dictionary.inventory.people
const recipient = await RecipientService.findById(recipientId) const person = await PersonService.findById(personId)
if (!recipient) { if (!person) {
return <div>{copy.edit.notFound}</div> return <div>{copy.edit.notFound}</div>
} }
@@ -22,8 +22,8 @@ export default async function RecipientEditPage({
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<h1 className="text-2xl font-bold">{copy.edit.title}</h1> <h1 className="text-2xl font-bold">{copy.edit.title}</h1>
</div> </div>
<RecipientForm <PersonForm
initialData={recipient} initialData={person}
mode="edit" mode="edit"
formCopy={copy.form} formCopy={copy.form}
schemaCopy={copy.schema} schemaCopy={copy.schema}
@@ -1,23 +1,23 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { getI18n } from "@/i18n/server" import { getI18n } from "@/i18n/server"
import { AssignmentService } from "@/services/assignment.service" import { AssignmentService } from "@/services/assignment.service"
import { RecipientService } from "@/services/recipient.service" import { PersonService } from "@/services/person.service"
import { formatRecipientDepartment } from "../_components/recipient.copy" import { formatPersonDepartment } from "../_components/person.copy"
export default async function RecipientInfoPage({ export default async function PersonInfoPage({
params, params,
}: { }: {
params: Promise<{ recipientId: string }> params: Promise<{ personId: string }>
}) { }) {
const { recipientId } = await params const { personId } = await params
const { dictionary } = await getI18n() const { dictionary } = await getI18n()
const copy = dictionary.inventory.recipients const copy = dictionary.inventory.people
const assignmentCopy = dictionary.inventory.assignments const assignmentCopy = dictionary.inventory.assignments
const recipient = await RecipientService.findById(recipientId) const person = await PersonService.findById(personId)
const assignments = await AssignmentService.findAllByRecipient(recipientId) const assignments = await AssignmentService.findAllByPerson(personId)
if (!recipient) { if (!person) {
return <div>{copy.detail.notFound}</div> return <div>{copy.detail.notFound}</div>
} }
@@ -25,33 +25,25 @@ export default async function RecipientInfoPage({
<div className="grid gap-6"> <div className="grid gap-6">
<Card className="rounded-sm shadow-none"> <Card className="rounded-sm shadow-none">
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>{`${person.firstName} ${person.lastName}`}</CardTitle>
{`${recipient.firstName} ${recipient.lastName}`}
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-sm"> <div className="grid grid-cols-2 gap-x-8 gap-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">
{copy.detail.labels.username}
</span>
<span>{recipient.username}</span>
</div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">{copy.detail.labels.email}</span> <span className="text-gray-600">{copy.detail.labels.email}</span>
<span>{recipient.email}</span> <span>{person.email}</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">{copy.detail.labels.phone}</span> <span className="text-gray-600">{copy.detail.labels.phone}</span>
<span>{recipient.phone}</span> <span>{person.phone}</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600"> <span className="text-gray-600">
{copy.detail.labels.department} {copy.detail.labels.department}
</span> </span>
<span> <span>
{formatRecipientDepartment( {formatPersonDepartment(
recipient.department, person.department,
copy.departments, copy.departments,
copy.fallback, copy.fallback,
)} )}
@@ -0,0 +1,22 @@
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
}
@@ -5,42 +5,39 @@ import { useRouter } from "next/navigation"
import { useMemo } from "react" import { useMemo } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { toast } from "sonner" import { toast } from "sonner"
import { import { createNewPerson, updatePerson } from "@/actions/person.actions"
createNewRecipient,
updateRecipient,
} from "@/actions/recipient.actions"
import { import {
SubmitButton, SubmitButton,
type SubmitButtonCopy, type SubmitButtonCopy,
} from "@/components/forms/submitButton" } from "@/components/forms/submitButton"
import { RECIPIENT_DEPARTMENTS } from "@/lib/constants" import { PERSON_DEPARTMENTS } from "@/lib/constants"
import { import {
buildCreateRecipientSchema, buildCreatePersonSchema,
buildUpdateRecipientSchema, buildUpdatePersonSchema,
type CreateRecipientFormType, type CreatePersonFormType,
type RecipientSchemaCopy, type PersonSchemaCopy,
type UpdateRecipientFormType, type UpdatePersonFormType,
} from "@/schemas/recipient.schema" } from "@/schemas/person.schema"
import type { Recipient } from "@/types" import type { Person } from "@/types"
import { import {
formatRecipientDepartment, formatPersonDepartment,
type RecipientDepartmentCopy, type PersonDepartmentCopy,
type RecipientFallbackCopy, type PersonFallbackCopy,
type RecipientFormCopy, type PersonFormCopy,
} from "./recipient.copy" } from "./person.copy"
interface RecipientFormProps { interface PersonFormProps {
initialData?: Recipient initialData?: Person
mode?: "create" | "edit" mode?: "create" | "edit"
formCopy: RecipientFormCopy formCopy: PersonFormCopy
schemaCopy: RecipientSchemaCopy schemaCopy: PersonSchemaCopy
departmentCopy: RecipientDepartmentCopy departmentCopy: PersonDepartmentCopy
fallbackCopy: RecipientFallbackCopy fallbackCopy: PersonFallbackCopy
submitButtonCopy: SubmitButtonCopy submitButtonCopy: SubmitButtonCopy
} }
export default function RecipientForm({ export default function PersonForm({
initialData, initialData,
mode = "create", mode = "create",
formCopy, formCopy,
@@ -48,13 +45,13 @@ export default function RecipientForm({
departmentCopy, departmentCopy,
fallbackCopy, fallbackCopy,
submitButtonCopy, submitButtonCopy,
}: RecipientFormProps) { }: PersonFormProps) {
const router = useRouter() const router = useRouter()
const schema = useMemo( const schema = useMemo(
() => () =>
mode === "create" mode === "create"
? buildCreateRecipientSchema(schemaCopy) ? buildCreatePersonSchema(schemaCopy)
: buildUpdateRecipientSchema(schemaCopy), : buildUpdatePersonSchema(schemaCopy),
[mode, schemaCopy], [mode, schemaCopy],
) )
@@ -63,11 +60,10 @@ export default function RecipientForm({
handleSubmit, handleSubmit,
setError, setError,
formState: { errors, isSubmitting, isSubmitSuccessful }, formState: { errors, isSubmitting, isSubmitSuccessful },
} = useForm<CreateRecipientFormType>({ } = useForm<CreatePersonFormType>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
id: initialData?.id || "", id: initialData?.id || "",
username: initialData?.username || "",
firstName: initialData?.firstName || "", firstName: initialData?.firstName || "",
lastName: initialData?.lastName || "", lastName: initialData?.lastName || "",
department: initialData?.department || "OTHER", department: initialData?.department || "OTHER",
@@ -76,16 +72,16 @@ export default function RecipientForm({
}, },
}) })
const onSubmit = async (formData: CreateRecipientFormType) => { const onSubmit = async (formData: CreatePersonFormType) => {
const response = const response =
mode === "create" mode === "create"
? await createNewRecipient(formData) ? await createNewPerson(formData)
: await updateRecipient(formData as UpdateRecipientFormType) : await updatePerson(formData as UpdatePersonFormType)
if (response?.errors) { if (response?.errors) {
Object.entries(response.errors).forEach(([fieldName, messages]) => { Object.entries(response.errors).forEach(([fieldName, messages]) => {
messages.forEach((msg: string) => { messages.forEach((msg: string) => {
setError(fieldName as keyof CreateRecipientFormType, { setError(fieldName as keyof CreatePersonFormType, {
type: "server", type: "server",
message: msg, message: msg,
}) })
@@ -97,28 +93,13 @@ export default function RecipientForm({
if (response?.success) { if (response?.success) {
toast.success(response.message) toast.success(response.message)
router.push("/recipients") router.push("/people")
} }
} }
return ( return (
<form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}> <form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
<input type="hidden" {...register("id")} /> <input type="hidden" {...register("id")} />
<div>
<label htmlFor="username" className="mb-2 block text-lg">
{formCopy.usernameLabel}
</label>
<input
type="text"
id="username"
placeholder={formCopy.usernamePlaceholder}
{...register("username")}
className={`w-full rounded-lg border px-4 py-2`}
/>
{errors?.username && (
<p className="text-error">{errors.username.message}</p>
)}
</div>
<div> <div>
<label htmlFor="firstName" className="mb-2 block text-lg"> <label htmlFor="firstName" className="mb-2 block text-lg">
{formCopy.firstNameLabel} {formCopy.firstNameLabel}
@@ -159,13 +140,9 @@ export default function RecipientForm({
className="w-full rounded-lg border px-4 py-2" className="w-full rounded-lg border px-4 py-2"
> >
<option value="">{formCopy.departmentPlaceholder}</option> <option value="">{formCopy.departmentPlaceholder}</option>
{Object.keys(RECIPIENT_DEPARTMENTS).map((department) => ( {Object.keys(PERSON_DEPARTMENTS).map((department) => (
<option key={department} value={department}> <option key={department} value={department}>
{formatRecipientDepartment( {formatPersonDepartment(department, departmentCopy, fallbackCopy)}
department,
departmentCopy,
fallbackCopy,
)}
</option> </option>
))} ))}
</select> </select>
@@ -1,17 +1,17 @@
import { getI18n } from "@/i18n/server" import { getI18n } from "@/i18n/server"
import RecipientForm from "../_components/recipient.form" import PersonForm from "../_components/person.form"
export default async function NewRecipientPage() { export default async function NewPersonPage() {
const { dictionary } = await getI18n() const { dictionary } = await getI18n()
const copy = dictionary.inventory.recipients const copy = dictionary.inventory.people
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<h1 className="text-2xl font-bold">{copy.new.title}</h1> <h1 className="text-2xl font-bold">{copy.new.title}</h1>
</div> </div>
<RecipientForm <PersonForm
mode="create" mode="create"
formCopy={copy.form} formCopy={copy.form}
schemaCopy={copy.schema} schemaCopy={copy.schema}
@@ -4,13 +4,13 @@ import Link from "next/link"
import PageHeader from "@/components/common/pageheader" import PageHeader from "@/components/common/pageheader"
import PaginationButtons from "@/components/common/pagination" import PaginationButtons from "@/components/common/pagination"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import type { Recipient } from "@/generated/prisma/client" import type { Person } from "@/generated/prisma/client"
import { getI18n } from "@/i18n/server" import { getI18n } from "@/i18n/server"
import { RecipientService } from "@/services/recipient.service" import { PersonService } from "@/services/person.service"
import { formatRecipientDepartment } from "./_components/recipient.copy" import { formatPersonDepartment } from "./_components/person.copy"
export default async function RecipientsPage(props: { export default async function PeoplePage(props: {
searchParams?: Promise<{ searchParams?: Promise<{
page?: string page?: string
search?: string search?: string
@@ -19,33 +19,29 @@ export default async function RecipientsPage(props: {
const searchParams = await props.searchParams const searchParams = await props.searchParams
const currentPage = searchParams?.page ? parseInt(searchParams.page, 10) : 1 const currentPage = searchParams?.page ? parseInt(searchParams.page, 10) : 1
const search = searchParams?.search || "" const search = searchParams?.search || ""
const { data: recipients, totalPages } = const { data: people, totalPages } = await PersonService.findAllPaginated({
await RecipientService.findAllPaginated({ page: currentPage,
page: currentPage, pageSize: 10,
pageSize: 10, search,
search, })
})
const { dictionary } = await getI18n() const { dictionary } = await getI18n()
const copy = dictionary.inventory.recipients const copy = dictionary.inventory.people
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<PageHeader <PageHeader
title={copy.list.title} title={copy.list.title}
link="/recipients/new" link="/people/new"
addLabel={copy.list.addLabel} addLabel={copy.list.addLabel}
data={recipients} data={people}
search={search} search={search}
/> />
{recipients.length === 0 && <div>{copy.list.empty}</div>} {people.length === 0 && <div>{copy.list.empty}</div>}
{recipients.length > 0 && ( {people.length > 0 && (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="text-muted-foreground w-full text-left text-sm"> <table className="text-muted-foreground w-full text-left text-sm">
<thead className="border-b"> <thead className="border-b">
<tr> <tr>
<th scope="col" className="p-4">
{copy.list.columns.username}
</th>
<th scope="col" className="p-4"> <th scope="col" className="p-4">
{copy.list.columns.name} {copy.list.columns.name}
</th> </th>
@@ -64,23 +60,22 @@ export default async function RecipientsPage(props: {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{recipients.map((recipient: Recipient) => ( {people.map((person: Person) => (
<tr key={recipient.id} className="border-b"> <tr key={person.id} className="border-b">
<td className="p-4">{recipient.username}</td>
<td className="p-4"> <td className="p-4">
{`${recipient.firstName} ${recipient.lastName}`} {`${person.firstName} ${person.lastName}`}
</td> </td>
<td className="p-4">{recipient.email}</td> <td className="p-4">{person.email}</td>
<td className="p-4">{recipient.phone}</td> <td className="p-4">{person.phone}</td>
<td className="p-4"> <td className="p-4">
{formatRecipientDepartment( {formatPersonDepartment(
recipient.department, person.department,
copy.departments, copy.departments,
copy.fallback, copy.fallback,
)} )}
</td> </td>
<td className="flex items-center gap-2 p-4"> <td className="flex items-center gap-2 p-4">
<Link href={`/recipients/${recipient.id}`} passHref> <Link href={`/people/${person.id}`} passHref>
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
@@ -89,7 +84,7 @@ export default async function RecipientsPage(props: {
<Eye /> <Eye />
</Button> </Button>
</Link> </Link>
<Link href={`/recipients/${recipient.id}/edit`} passHref> <Link href={`/people/${person.id}/edit`} passHref>
<Button <Button
className="btn btn-primary" className="btn btn-primary"
variant="outline" variant="outline"
@@ -105,7 +100,7 @@ export default async function RecipientsPage(props: {
</tbody> </tbody>
<tfoot className="border-t"> <tfoot className="border-t">
<tr> <tr>
<td colSpan={6} className="p-4 text-center text-sm"> <td colSpan={5} className="p-4 text-center text-sm">
<PaginationButtons totalPages={totalPages} /> <PaginationButtons totalPages={totalPages} />
</td> </td>
</tr> </tr>
@@ -1,24 +0,0 @@
import type { Dictionary } from "@/i18n/dictionaries"
export type RecipientListCopy = Dictionary["inventory"]["recipients"]["list"]
export type RecipientDetailCopy =
Dictionary["inventory"]["recipients"]["detail"]
export type RecipientFormCopy = Dictionary["inventory"]["recipients"]["form"]
export type RecipientDepartmentCopy =
Dictionary["inventory"]["recipients"]["departments"]
export type RecipientFallbackCopy =
Dictionary["inventory"]["recipients"]["fallback"]
export function formatRecipientDepartment(
department: string | null | undefined,
departmentCopy: RecipientDepartmentCopy,
fallbackCopy: RecipientFallbackCopy,
) {
if (!department) {
return fallbackCopy.unknownDepartment
}
return department in departmentCopy
? departmentCopy[department as keyof RecipientDepartmentCopy]
: fallbackCopy.unknownDepartment
}
+2 -2
View File
@@ -37,8 +37,8 @@ const items: { key: keyof AddMenuCopy; href: string }[] = [
href: "/inventory/assets/new", href: "/inventory/assets/new",
}, },
{ {
key: "recipient", key: "person",
href: "/recipients/new", href: "/people/new",
}, },
{ {
key: "assignment", key: "assignment",
+2 -2
View File
@@ -74,8 +74,8 @@ const items: SidebarItem[] = [
}, },
{ {
type: "item", type: "item",
labelKey: "recipients", labelKey: "people",
url: "/recipients", url: "/people",
icon: User, icon: User,
}, },
{ {
+7 -1
View File
@@ -37,6 +37,7 @@ export const en = {
categories: "Categories", categories: "Categories",
assets: "Assets", assets: "Assets",
recipients: "Recipients", recipients: "Recipients",
people: "People",
movements: "Movements", movements: "Movements",
assignments: "Assignments", assignments: "Assignments",
users: "Users", users: "Users",
@@ -51,6 +52,7 @@ export const en = {
item: "Item", item: "Item",
asset: "Asset", asset: "Asset",
recipient: "Recipient", recipient: "Recipient",
person: "Person",
assignment: "Assignment", assignment: "Assignment",
}, },
resetDatabase: { resetDatabase: {
@@ -610,8 +612,12 @@ export const en = {
title: "Total Recipients", title: "Total Recipients",
countLabel: "Total", countLabel: "Total",
}, },
people: {
title: "Total People",
countLabel: "Total",
},
}, },
}, },
} }
export type Dictionary = typeof en export type Dictionary = typeof en
+7 -1
View File
@@ -39,6 +39,7 @@ export const es = {
categories: "Categorías", categories: "Categorías",
assets: "Activos", assets: "Activos",
recipients: "Destinatarios", recipients: "Destinatarios",
people: "Personas",
movements: "Movimientos", movements: "Movimientos",
assignments: "Asignaciones", assignments: "Asignaciones",
users: "Usuarios", users: "Usuarios",
@@ -53,6 +54,7 @@ export const es = {
item: "Artículo", item: "Artículo",
asset: "Activo", asset: "Activo",
recipient: "Destinatario", recipient: "Destinatario",
person: "Persona",
assignment: "Asignación", assignment: "Asignación",
}, },
resetDatabase: { resetDatabase: {
@@ -615,6 +617,10 @@ export const es = {
title: "Total de destinatarios", title: "Total de destinatarios",
countLabel: "Total", countLabel: "Total",
}, },
people: {
title: "Total de personas",
countLabel: "Total",
},
}, },
}, },
} satisfies Dictionary } satisfies Dictionary
+1 -1
View File
@@ -71,4 +71,4 @@ export const updatePersonSchema = buildUpdatePersonSchema(
defaultPersonSchemaCopy, defaultPersonSchemaCopy,
) )
export type UpdatePersonFormType = z.infer<typeof updatePersonSchema> export type UpdatePersonFormType = z.infer<typeof updatePersonSchema>
+1 -1
View File
@@ -143,4 +143,4 @@ export const AssignmentService = {
data, data,
}) })
}, },
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import type { Prisma, Person } from "@/generated/prisma/client" import type { Person, Prisma } from "@/generated/prisma/client"
import { paginate } from "@/lib/paginate" import { paginate } from "@/lib/paginate"
import prisma from "@/lib/prisma" import prisma from "@/lib/prisma"
@@ -68,4 +68,4 @@ export const PersonService = {
): Promise<Person> => { ): Promise<Person> => {
return db.person.update({ where: { id }, data }) return db.person.update({ where: { id }, data })
}, },
} }
+1 -1
View File
@@ -13,4 +13,4 @@ export type AssignmentWithRecipientItemAsset = Assignment & {
recipient: Person | null recipient: Person | null
item: Item | null item: Item | null
asset: Asset | null asset: Asset | null
} }
+1 -1
View File
@@ -1,3 +1,3 @@
import type { Person as PrismaPerson } from "@/generated/prisma/client" import type { Person as PrismaPerson } from "@/generated/prisma/client"
export type Person = PrismaPerson export type Person = PrismaPerson
+4 -2
View File
@@ -106,7 +106,9 @@ export async function updatePersonUseCase(
department, department,
email: email || null, email: email || null,
phone: phone || null, phone: phone || null,
...(userId ? { user: { connect: { id: userId } } } : { userId: null }), ...(userId
? { user: { connect: { id: userId } } }
: { userId: null }),
}, },
tx, tx,
) )
@@ -124,4 +126,4 @@ export async function updatePersonUseCase(
throw error throw error
} }
} }
+2 -2
View File
@@ -1,6 +1,6 @@
import type { import type {
PrismaClient,
PersonDepartment, PersonDepartment,
PrismaClient,
UserRole, UserRole,
} from "@/generated/prisma/client" } from "@/generated/prisma/client"
@@ -90,4 +90,4 @@ export async function createTestItem(
category: { connect: { id: categoryId } }, category: { connect: { id: categoryId } },
}, },
}) })
} }
@@ -187,4 +187,4 @@ describe("person use-cases", () => {
expect(nameResults.data).toHaveLength(1) expect(nameResults.data).toHaveLength(1)
expect(nameResults.data[0].firstName).toBe("Bob") expect(nameResults.data[0].firstName).toBe("Bob")
}) })
}) })
@@ -23,8 +23,8 @@ vi.mock("@/i18n/server", () => ({
getI18n: mocks.getI18n, getI18n: mocks.getI18n,
})) }))
vi.mock("@/services/recipient.service", () => ({ vi.mock("@/services/person.service", () => ({
RecipientService: { PersonService: {
findAll: mocks.findAllRecipients, findAll: mocks.findAllRecipients,
}, },
})) }))
@@ -0,0 +1,127 @@
import { renderToStaticMarkup } from "react-dom/server"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { en } from "@/i18n/dictionaries/en"
import { es } from "@/i18n/dictionaries/es"
const mocks = vi.hoisted(() => ({
getI18n: vi.fn(),
findById: vi.fn(),
createNewPerson: vi.fn(),
updatePerson: vi.fn(),
push: vi.fn(),
toastError: vi.fn(),
toastSuccess: vi.fn(),
}))
vi.mock("@/i18n/server", () => ({
getI18n: mocks.getI18n,
}))
vi.mock("@/services/person.service", () => ({
PersonService: {
findById: mocks.findById,
},
}))
vi.mock("@/actions/person.actions", () => ({
createNewPerson: mocks.createNewPerson,
updatePerson: mocks.updatePerson,
}))
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: mocks.push,
}),
}))
vi.mock("sonner", () => ({
toast: {
error: mocks.toastError,
success: mocks.toastSuccess,
},
}))
describe("person form pages", () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" })
})
it("renders the new person page with Person form copy and no username field", async () => {
const { default: NewPersonPage } = await import(
"@/app/(dashboard)/people/new/page"
)
const html = renderToStaticMarkup(await NewPersonPage())
// Person form, not Recipient
expect(html).toContain("Agregar persona")
// No username label or placeholder
expect(html).not.toContain("Usuario")
expect(html).not.toContain('placeholder="Usuario"')
// Has expected person form fields
expect(html).toContain("Nombre")
expect(html).toContain("Apellido")
expect(html).toContain("Selecciona un departamento")
expect(html).toContain('option value="ENGINEERING"')
expect(html).toContain(">Ingeniería</option>")
// Has department options from PERSON_DEPARTMENTS
expect(html).toContain("Correo electrónico")
expect(html).toContain("Teléfono")
expect(html).toContain("Crear persona")
})
it("renders the edit person page with Person heading and no username", async () => {
const { default: PersonEditPage } = await import(
"@/app/(dashboard)/people/[personId]/edit/page"
)
mocks.findById.mockResolvedValue({
id: "person-1",
firstName: "Ada",
lastName: "Lovelace",
email: "ada@example.test",
phone: "1234",
department: "ENGINEERING",
})
const html = renderToStaticMarkup(
await PersonEditPage({
params: Promise.resolve({ personId: "person-1" }),
}),
)
expect(html).toContain("Editar persona")
expect(html).toContain("Actualizar persona")
expect(html).not.toContain("Usuario")
})
it("renders a Person not-found message on edit page", async () => {
const { default: PersonEditPage } = await import(
"@/app/(dashboard)/people/[personId]/edit/page"
)
mocks.findById.mockResolvedValue(null)
const html = renderToStaticMarkup(
await PersonEditPage({
params: Promise.resolve({ personId: "missing-person" }),
}),
)
expect(html).toContain("Persona no encontrada")
})
it("wires English Person form submit copy through the new page", async () => {
const { default: NewPersonPage } = await import(
"@/app/(dashboard)/people/new/page"
)
mocks.getI18n.mockResolvedValueOnce({ dictionary: en, locale: "en" })
const html = renderToStaticMarkup(await NewPersonPage())
expect(html).toContain("Create Person")
})
})
@@ -0,0 +1,81 @@
import { createElement } from "react"
import { renderToStaticMarkup } from "react-dom/server"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { en } from "@/i18n/dictionaries/en"
import { es } from "@/i18n/dictionaries/es"
const mocks = vi.hoisted(() => ({
getI18n: vi.fn(),
findById: vi.fn(),
personForm: vi.fn(),
}))
vi.mock("@/i18n/server", () => ({
getI18n: mocks.getI18n,
}))
vi.mock("@/services/person.service", () => ({
PersonService: {
findById: mocks.findById,
},
}))
vi.mock("@/app/(dashboard)/people/_components/person.form", () => ({
default: (props: unknown) => {
mocks.personForm(props)
return createElement("div", null, "Person form")
},
}))
describe("person form schema wiring", () => {
beforeEach(() => {
vi.clearAllMocks()
})
it("passes server-resolved Person schema copy into the new person form boundary", async () => {
mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" })
const { default: NewPersonPage } = await import(
"@/app/(dashboard)/people/new/page"
)
renderToStaticMarkup(await NewPersonPage())
expect(mocks.personForm).toHaveBeenCalledWith(
expect.objectContaining({
mode: "create",
schemaCopy: es.inventory.people.schema,
}),
)
})
it("passes server-resolved Person schema copy into the edit person form boundary", async () => {
mocks.getI18n.mockResolvedValue({ dictionary: en, locale: "en" })
mocks.findById.mockResolvedValue({
id: "person-1",
firstName: "Ada",
lastName: "Lovelace",
department: "ENGINEERING",
email: "ada@example.test",
phone: "1234",
})
const { default: PersonEditPage } = await import(
"@/app/(dashboard)/people/[personId]/edit/page"
)
renderToStaticMarkup(
await PersonEditPage({
params: Promise.resolve({ personId: "person-1" }),
}),
)
expect(mocks.personForm).toHaveBeenCalledWith(
expect.objectContaining({
mode: "edit",
schemaCopy: en.inventory.people.schema,
}),
)
})
})
+163
View File
@@ -0,0 +1,163 @@
import { createElement } from "react"
import { renderToStaticMarkup } from "react-dom/server"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { en } from "@/i18n/dictionaries/en"
const mocks = vi.hoisted(() => ({
findAllPaginated: vi.fn(),
findById: vi.fn(),
findAllByPerson: vi.fn(),
getI18n: vi.fn(),
}))
vi.mock("@/i18n/server", () => ({
getI18n: mocks.getI18n,
}))
vi.mock("@/services/person.service", () => ({
PersonService: {
findAllPaginated: mocks.findAllPaginated,
findById: mocks.findById,
},
}))
vi.mock("@/services/assignment.service", () => ({
AssignmentService: {
findAllByPerson: mocks.findAllByPerson,
},
}))
vi.mock("@/components/common/pageheader", () => ({
default: ({ title, addLabel }: { title: string; addLabel?: string }) =>
createElement(
"header",
null,
[title, addLabel].filter(Boolean).join(" | "),
),
}))
vi.mock("@/components/common/pagination", () => ({
default: ({ totalPages }: { totalPages: number }) =>
createElement("nav", { "aria-label": "Pagination" }, totalPages),
}))
describe("person pages", () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.getI18n.mockResolvedValue({ dictionary: en, locale: "en" })
})
it("renders the person list page with Person data and no username column", async () => {
const { default: PeoplePage } = await import(
"@/app/(dashboard)/people/page"
)
mocks.findAllPaginated.mockResolvedValue({
data: [
{
id: "person-1",
firstName: "Ada",
lastName: "Lovelace",
email: "ada@example.test",
phone: "1234",
department: "ENGINEERING",
},
],
totalPages: 1,
})
const html = renderToStaticMarkup(
await PeoplePage({ searchParams: Promise.resolve({}) }),
)
// Uses Person copy (inventory.people), not Recipient copy
expect(html).toContain("People")
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
expect(html).not.toContain(">ada<")
// Name and other fields rendered
expect(html).toContain("Ada Lovelace")
expect(html).toContain("Engineering")
// Links point to /people, not /recipients
expect(html).toContain("/people/person-1")
expect(html).toContain("/people/person-1/edit")
})
it("renders the person list empty state from Person copy", async () => {
const { default: PeoplePage } = await import(
"@/app/(dashboard)/people/page"
)
mocks.findAllPaginated.mockResolvedValue({
data: [],
totalPages: 0,
})
const html = renderToStaticMarkup(
await PeoplePage({ searchParams: Promise.resolve({}) }),
)
expect(html).toContain("No people found.")
})
it("renders person detail page without username and uses PersonService + AssignmentService.findAllByPerson", async () => {
const { default: PersonInfoPage } = await import(
"@/app/(dashboard)/people/[personId]/page"
)
mocks.findById.mockResolvedValue({
id: "person-1",
firstName: "Ada",
lastName: "Lovelace",
email: "ada@example.test",
phone: "1234",
department: "DRIVER",
})
mocks.findAllByPerson.mockResolvedValue([
{
id: "assignment-1",
item: { name: "Laptop" },
asset: { serialNumber: "SN-001" },
quantity: 1,
},
])
const html = renderToStaticMarkup(
await PersonInfoPage({
params: Promise.resolve({ personId: "person-1" }),
}),
)
// No username label or value
expect(html).not.toContain("Username")
expect(html).not.toContain(">ada<")
// Person detail fields
expect(html).toContain("Email")
expect(html).toContain("Phone")
expect(html).toContain("Department")
expect(html).toContain("ada@example.test")
expect(html).toContain("Driver")
// Embedded assignments
expect(html).toContain("Laptop")
})
it("renders person detail not-found from Person copy", async () => {
const { default: PersonInfoPage } = await import(
"@/app/(dashboard)/people/[personId]/page"
)
mocks.findById.mockResolvedValue(null)
mocks.findAllByPerson.mockResolvedValue([])
const html = renderToStaticMarkup(
await PersonInfoPage({
params: Promise.resolve({ personId: "missing-person" }),
}),
)
expect(html).toContain("Person not found")
})
})
+12
View File
@@ -53,6 +53,7 @@ describe("i18n dictionaries", () => {
categories: "Categories", categories: "Categories",
assets: "Assets", assets: "Assets",
recipients: "Recipients", recipients: "Recipients",
people: "People",
movements: "Movements", movements: "Movements",
assignments: "Assignments", assignments: "Assignments",
users: "Users", users: "Users",
@@ -67,6 +68,7 @@ describe("i18n dictionaries", () => {
item: "Item", item: "Item",
asset: "Asset", asset: "Asset",
recipient: "Recipient", recipient: "Recipient",
person: "Person",
assignment: "Assignment", assignment: "Assignment",
}, },
resetDatabase: { resetDatabase: {
@@ -88,6 +90,7 @@ describe("i18n dictionaries", () => {
categories: "Categorías", categories: "Categorías",
assets: "Activos", assets: "Activos",
recipients: "Destinatarios", recipients: "Destinatarios",
people: "Personas",
movements: "Movimientos", movements: "Movimientos",
assignments: "Asignaciones", assignments: "Asignaciones",
users: "Usuarios", users: "Usuarios",
@@ -102,6 +105,7 @@ describe("i18n dictionaries", () => {
item: "Artículo", item: "Artículo",
asset: "Activo", asset: "Activo",
recipient: "Destinatario", recipient: "Destinatario",
person: "Persona",
assignment: "Asignación", assignment: "Asignación",
}, },
resetDatabase: { resetDatabase: {
@@ -940,6 +944,10 @@ describe("i18n dictionaries", () => {
title: "Total Recipients", title: "Total Recipients",
countLabel: "Total", countLabel: "Total",
}, },
people: {
title: "Total People",
countLabel: "Total",
},
}, },
}) })
@@ -958,6 +966,10 @@ describe("i18n dictionaries", () => {
title: "Total de destinatarios", title: "Total de destinatarios",
countLabel: "Total", countLabel: "Total",
}, },
people: {
title: "Total de personas",
countLabel: "Total",
},
}, },
}) })
}) })