feat(i18n): localize recipients UI
This commit is contained in:
@@ -9,21 +9,25 @@ export default async function RecipientEditPage({
|
||||
params: Promise<{ recipientId: string }>
|
||||
}) {
|
||||
const { recipientId } = await params
|
||||
const recipient = await RecipientService.findById(recipientId)
|
||||
const { dictionary } = await getI18n()
|
||||
const copy = dictionary.inventory.recipients
|
||||
const recipient = await RecipientService.findById(recipientId)
|
||||
|
||||
if (!recipient) {
|
||||
return <div>Recipient not found</div>
|
||||
return <div>{copy.edit.notFound}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h1 className="text-2xl font-bold">Edit Recipient</h1>
|
||||
<h1 className="text-2xl font-bold">{copy.edit.title}</h1>
|
||||
</div>
|
||||
<RecipientForm
|
||||
initialData={recipient}
|
||||
mode="edit"
|
||||
formCopy={copy.form}
|
||||
departmentCopy={copy.departments}
|
||||
fallbackCopy={copy.fallback}
|
||||
submitButtonCopy={dictionary.common.submitButton}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { getI18n } from "@/i18n/server"
|
||||
import { AssignmentService } from "@/services/assignment.service"
|
||||
import { RecipientService } from "@/services/recipient.service"
|
||||
|
||||
import { formatRecipientDepartment } from "../_components/recipient.copy"
|
||||
|
||||
export default async function RecipientInfoPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ recipientId: string }>
|
||||
}) {
|
||||
const { recipientId } = await params
|
||||
const { dictionary } = await getI18n()
|
||||
const copy = dictionary.inventory.recipients
|
||||
const recipient = await RecipientService.findById(recipientId)
|
||||
const assignments = await AssignmentService.findAllByRecipient(recipientId)
|
||||
|
||||
if (!recipient) {
|
||||
return <div>Recipient not found</div>
|
||||
return <div>{copy.detail.notFound}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -26,20 +31,30 @@ export default async function RecipientInfoPage({
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Username</span>
|
||||
<span className="text-gray-600">
|
||||
{copy.detail.labels.username}
|
||||
</span>
|
||||
<span>{recipient.username}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Email</span>
|
||||
<span className="text-gray-600">{copy.detail.labels.email}</span>
|
||||
<span>{recipient.email}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Phone</span>
|
||||
<span className="text-gray-600">{copy.detail.labels.phone}</span>
|
||||
<span>{recipient.phone}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Department</span>
|
||||
<span>{recipient.department}</span>
|
||||
<span className="text-gray-600">
|
||||
{copy.detail.labels.department}
|
||||
</span>
|
||||
<span>
|
||||
{formatRecipientDepartment(
|
||||
recipient.department,
|
||||
copy.departments,
|
||||
copy.fallback,
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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
|
||||
}
|
||||
@@ -20,15 +20,28 @@ import {
|
||||
} from "@/schemas/recipient.schema"
|
||||
import type { Recipient } from "@/types"
|
||||
|
||||
import {
|
||||
formatRecipientDepartment,
|
||||
type RecipientDepartmentCopy,
|
||||
type RecipientFallbackCopy,
|
||||
type RecipientFormCopy,
|
||||
} from "./recipient.copy"
|
||||
|
||||
interface RecipientFormProps {
|
||||
initialData?: Recipient
|
||||
mode?: "create" | "edit"
|
||||
formCopy: RecipientFormCopy
|
||||
departmentCopy: RecipientDepartmentCopy
|
||||
fallbackCopy: RecipientFallbackCopy
|
||||
submitButtonCopy: SubmitButtonCopy
|
||||
}
|
||||
|
||||
export default function RecipientForm({
|
||||
initialData,
|
||||
mode = "create",
|
||||
formCopy,
|
||||
departmentCopy,
|
||||
fallbackCopy,
|
||||
submitButtonCopy,
|
||||
}: RecipientFormProps) {
|
||||
const router = useRouter()
|
||||
@@ -81,12 +94,12 @@ export default function RecipientForm({
|
||||
<input type="hidden" {...register("id")} />
|
||||
<div>
|
||||
<label htmlFor="username" className="mb-2 block text-lg">
|
||||
Username
|
||||
{formCopy.usernameLabel}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
placeholder="Username"
|
||||
placeholder={formCopy.usernamePlaceholder}
|
||||
{...register("username")}
|
||||
className={`w-full rounded-lg border px-4 py-2`}
|
||||
/>
|
||||
@@ -96,12 +109,12 @@ export default function RecipientForm({
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="firstName" className="mb-2 block text-lg">
|
||||
First Name
|
||||
{formCopy.firstNameLabel}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
placeholder="First Name"
|
||||
placeholder={formCopy.firstNamePlaceholder}
|
||||
{...register("firstName")}
|
||||
className={`w-full rounded-lg border px-4 py-2`}
|
||||
/>
|
||||
@@ -111,12 +124,12 @@ export default function RecipientForm({
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="lastName" className="mb-2 block text-lg">
|
||||
Last Name
|
||||
{formCopy.lastNameLabel}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
placeholder="Last Name"
|
||||
placeholder={formCopy.lastNamePlaceholder}
|
||||
{...register("lastName")}
|
||||
className={`w-full rounded-lg border px-4 py-2`}
|
||||
/>
|
||||
@@ -126,17 +139,21 @@ export default function RecipientForm({
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="department" className="mb-2 block text-lg">
|
||||
Department
|
||||
{formCopy.departmentLabel}
|
||||
</label>
|
||||
<select
|
||||
id="department"
|
||||
{...register("department")}
|
||||
className="w-full rounded-lg border px-4 py-2"
|
||||
>
|
||||
<option value="">Select a department</option>
|
||||
<option value="">{formCopy.departmentPlaceholder}</option>
|
||||
{Object.keys(RECIPIENT_DEPARTMENTS).map((department) => (
|
||||
<option key={department} value={department}>
|
||||
{department}
|
||||
{formatRecipientDepartment(
|
||||
department,
|
||||
departmentCopy,
|
||||
fallbackCopy,
|
||||
)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -146,12 +163,12 @@ export default function RecipientForm({
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email" className="mb-2 block text-lg">
|
||||
Email
|
||||
{formCopy.emailLabel}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="email"
|
||||
placeholder="Email"
|
||||
placeholder={formCopy.emailPlaceholder}
|
||||
{...register("email")}
|
||||
className={`w-full rounded-lg border px-4 py-2`}
|
||||
/>
|
||||
@@ -159,12 +176,12 @@ export default function RecipientForm({
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="phone" className="mb-2 block text-lg">
|
||||
Phone
|
||||
{formCopy.phoneLabel}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="phone"
|
||||
placeholder="Phone"
|
||||
placeholder={formCopy.phonePlaceholder}
|
||||
{...register("phone")}
|
||||
className={`w-full rounded-lg border px-4 py-2`}
|
||||
/>
|
||||
@@ -175,7 +192,7 @@ export default function RecipientForm({
|
||||
isSubmitting={isSubmitting}
|
||||
isSubmitSuccessful={isSubmitSuccessful}
|
||||
>
|
||||
{mode === "create" ? "Create Recipient" : "Update Recipient"}
|
||||
{mode === "create" ? formCopy.createSubmit : formCopy.updateSubmit}
|
||||
</SubmitButton>
|
||||
</form>
|
||||
)
|
||||
|
||||
@@ -4,14 +4,18 @@ import RecipientForm from "../_components/recipient.form"
|
||||
|
||||
export default async function NewRecipientPage() {
|
||||
const { dictionary } = await getI18n()
|
||||
const copy = dictionary.inventory.recipients
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h1 className="text-2xl font-bold">Add Recipient</h1>
|
||||
<h1 className="text-2xl font-bold">{copy.new.title}</h1>
|
||||
</div>
|
||||
<RecipientForm
|
||||
mode="create"
|
||||
formCopy={copy.form}
|
||||
departmentCopy={copy.departments}
|
||||
fallbackCopy={copy.fallback}
|
||||
submitButtonCopy={dictionary.common.submitButton}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,11 @@ import PageHeader from "@/components/common/pageheader"
|
||||
import PaginationButtons from "@/components/common/pagination"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import type { Recipient } from "@/generated/prisma/client"
|
||||
import { getI18n } from "@/i18n/server"
|
||||
import { RecipientService } from "@/services/recipient.service"
|
||||
|
||||
import { formatRecipientDepartment } from "./_components/recipient.copy"
|
||||
|
||||
export default async function RecipientsPage(props: {
|
||||
searchParams?: Promise<{
|
||||
page?: string
|
||||
@@ -22,38 +25,41 @@ export default async function RecipientsPage(props: {
|
||||
pageSize: 10,
|
||||
search,
|
||||
})
|
||||
const { dictionary } = await getI18n()
|
||||
const copy = dictionary.inventory.recipients
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<PageHeader
|
||||
title="Recipients"
|
||||
title={copy.list.title}
|
||||
link="/recipients/new"
|
||||
addLabel={copy.list.addLabel}
|
||||
data={recipients}
|
||||
search={search}
|
||||
/>
|
||||
{recipients.length === 0 && <div>No recipients found</div>}
|
||||
{recipients.length === 0 && <div>{copy.list.empty}</div>}
|
||||
{recipients.length > 0 && (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="text-muted-foreground w-full text-left text-sm">
|
||||
<thead className="border-b">
|
||||
<tr>
|
||||
<th scope="col" className="p-4">
|
||||
Username
|
||||
{copy.list.columns.username}
|
||||
</th>
|
||||
<th scope="col" className="p-4">
|
||||
Name
|
||||
{copy.list.columns.name}
|
||||
</th>
|
||||
<th scope="col" className="p-4">
|
||||
Email
|
||||
{copy.list.columns.email}
|
||||
</th>
|
||||
<th scope="col" className="p-4">
|
||||
Phone
|
||||
{copy.list.columns.phone}
|
||||
</th>
|
||||
<th scope="col" className="p-4">
|
||||
Department
|
||||
{copy.list.columns.department}
|
||||
</th>
|
||||
<th scope="col" className="p-4">
|
||||
Actions
|
||||
{copy.list.columns.actions}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -66,10 +72,20 @@ export default async function RecipientsPage(props: {
|
||||
</td>
|
||||
<td className="p-4">{recipient.email}</td>
|
||||
<td className="p-4">{recipient.phone}</td>
|
||||
<td className="p-4">{recipient.department}</td>
|
||||
<td className="p-4">
|
||||
{formatRecipientDepartment(
|
||||
recipient.department,
|
||||
copy.departments,
|
||||
copy.fallback,
|
||||
)}
|
||||
</td>
|
||||
<td className="flex items-center gap-2 p-4">
|
||||
<Link href={`/recipients/${recipient.id}`} passHref>
|
||||
<Button variant="outline" size="icon">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label={copy.list.actions.view}
|
||||
>
|
||||
<Eye />
|
||||
</Button>
|
||||
</Link>
|
||||
@@ -78,6 +94,7 @@ export default async function RecipientsPage(props: {
|
||||
className="btn btn-primary"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label={copy.list.actions.edit}
|
||||
>
|
||||
<Pencil />
|
||||
</Button>
|
||||
|
||||
@@ -255,6 +255,70 @@ export const en = {
|
||||
invalidUpdateStatus: "Invalid status",
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
list: {
|
||||
title: "Recipients",
|
||||
addLabel: "Add Recipient",
|
||||
empty: "No recipients found.",
|
||||
columns: {
|
||||
username: "Username",
|
||||
name: "Name",
|
||||
email: "Email",
|
||||
phone: "Phone",
|
||||
department: "Department",
|
||||
actions: "Actions",
|
||||
},
|
||||
actions: {
|
||||
view: "View recipient",
|
||||
edit: "Edit recipient",
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
notFound: "Recipient not found",
|
||||
labels: {
|
||||
username: "Username",
|
||||
email: "Email",
|
||||
phone: "Phone",
|
||||
department: "Department",
|
||||
},
|
||||
},
|
||||
new: {
|
||||
title: "Add Recipient",
|
||||
},
|
||||
edit: {
|
||||
title: "Edit Recipient",
|
||||
notFound: "Recipient not found",
|
||||
},
|
||||
form: {
|
||||
usernameLabel: "Username",
|
||||
usernamePlaceholder: "Username",
|
||||
firstNameLabel: "First Name",
|
||||
firstNamePlaceholder: "First name",
|
||||
lastNameLabel: "Last Name",
|
||||
lastNamePlaceholder: "Last name",
|
||||
departmentLabel: "Department",
|
||||
departmentPlaceholder: "Select a department",
|
||||
emailLabel: "Email",
|
||||
emailPlaceholder: "Email",
|
||||
phoneLabel: "Phone",
|
||||
phonePlaceholder: "Phone",
|
||||
createSubmit: "Create Recipient",
|
||||
updateSubmit: "Update Recipient",
|
||||
},
|
||||
fallback: {
|
||||
unknownDepartment: "Unknown department",
|
||||
},
|
||||
departments: {
|
||||
IT: "IT",
|
||||
ENGINEERING: "Engineering",
|
||||
LOGISTICS: "Logistics",
|
||||
TRAFFIC: "Traffic",
|
||||
DRIVER: "Driver",
|
||||
ADMINISTRATION: "Administration",
|
||||
SALES: "Sales",
|
||||
OTHER: "Other",
|
||||
},
|
||||
},
|
||||
movements: {
|
||||
list: {
|
||||
title: "Movements",
|
||||
|
||||
@@ -259,6 +259,70 @@ export const es = {
|
||||
invalidUpdateStatus: "Estado inválido",
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
list: {
|
||||
title: "Destinatarios",
|
||||
addLabel: "Agregar destinatario",
|
||||
empty: "No se encontraron destinatarios.",
|
||||
columns: {
|
||||
username: "Usuario",
|
||||
name: "Nombre",
|
||||
email: "Correo electrónico",
|
||||
phone: "Teléfono",
|
||||
department: "Departamento",
|
||||
actions: "Acciones",
|
||||
},
|
||||
actions: {
|
||||
view: "Ver destinatario",
|
||||
edit: "Editar destinatario",
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
notFound: "Destinatario no encontrado",
|
||||
labels: {
|
||||
username: "Usuario",
|
||||
email: "Correo electrónico",
|
||||
phone: "Teléfono",
|
||||
department: "Departamento",
|
||||
},
|
||||
},
|
||||
new: {
|
||||
title: "Agregar destinatario",
|
||||
},
|
||||
edit: {
|
||||
title: "Editar destinatario",
|
||||
notFound: "Destinatario no encontrado",
|
||||
},
|
||||
form: {
|
||||
usernameLabel: "Usuario",
|
||||
usernamePlaceholder: "Usuario",
|
||||
firstNameLabel: "Nombre",
|
||||
firstNamePlaceholder: "Nombre",
|
||||
lastNameLabel: "Apellido",
|
||||
lastNamePlaceholder: "Apellido",
|
||||
departmentLabel: "Departamento",
|
||||
departmentPlaceholder: "Selecciona un departamento",
|
||||
emailLabel: "Correo electrónico",
|
||||
emailPlaceholder: "Correo electrónico",
|
||||
phoneLabel: "Teléfono",
|
||||
phonePlaceholder: "Teléfono",
|
||||
createSubmit: "Crear destinatario",
|
||||
updateSubmit: "Actualizar destinatario",
|
||||
},
|
||||
fallback: {
|
||||
unknownDepartment: "Departamento desconocido",
|
||||
},
|
||||
departments: {
|
||||
IT: "IT",
|
||||
ENGINEERING: "Ingeniería",
|
||||
LOGISTICS: "Logística",
|
||||
TRAFFIC: "Tráfico",
|
||||
DRIVER: "Chofer",
|
||||
ADMINISTRATION: "Administración",
|
||||
SALES: "Ventas",
|
||||
OTHER: "Otro",
|
||||
},
|
||||
},
|
||||
movements: {
|
||||
list: {
|
||||
title: "Movimientos",
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
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(),
|
||||
createNewRecipient: vi.fn(),
|
||||
updateRecipient: vi.fn(),
|
||||
push: vi.fn(),
|
||||
toastError: vi.fn(),
|
||||
toastSuccess: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("@/i18n/server", () => ({
|
||||
getI18n: mocks.getI18n,
|
||||
}))
|
||||
|
||||
vi.mock("@/services/recipient.service", () => ({
|
||||
RecipientService: {
|
||||
findById: mocks.findById,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@/actions/recipient.actions", () => ({
|
||||
createNewRecipient: mocks.createNewRecipient,
|
||||
updateRecipient: mocks.updateRecipient,
|
||||
}))
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({
|
||||
push: mocks.push,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock("sonner", () => ({
|
||||
toast: {
|
||||
error: mocks.toastError,
|
||||
success: mocks.toastSuccess,
|
||||
},
|
||||
}))
|
||||
|
||||
describe("recipient form pages localization", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" })
|
||||
})
|
||||
|
||||
it("renders the new recipient page with localized form copy and canonical department option values", async () => {
|
||||
const { default: NewRecipientPage } = await import(
|
||||
"@/app/(dashboard)/recipients/new/page"
|
||||
)
|
||||
|
||||
const html = renderToStaticMarkup(await NewRecipientPage())
|
||||
|
||||
expect(html).toContain("Agregar destinatario")
|
||||
expect(html).toContain("Usuario")
|
||||
expect(html).toContain('placeholder="Usuario"')
|
||||
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>")
|
||||
expect(html).toContain("Correo electrónico")
|
||||
expect(html).toContain("Teléfono")
|
||||
expect(html).toContain("Crear destinatario")
|
||||
})
|
||||
|
||||
it("renders the edit recipient page with localized heading and submit text", async () => {
|
||||
const { default: RecipientEditPage } = await import(
|
||||
"@/app/(dashboard)/recipients/[recipientId]/edit/page"
|
||||
)
|
||||
|
||||
mocks.findById.mockResolvedValue({
|
||||
id: "recipient-1",
|
||||
username: "ada",
|
||||
firstName: "Ada",
|
||||
lastName: "Lovelace",
|
||||
email: "ada@example.test",
|
||||
phone: "1234",
|
||||
department: "ENGINEERING",
|
||||
})
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
await RecipientEditPage({
|
||||
params: Promise.resolve({ recipientId: "recipient-1" }),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain("Editar destinatario")
|
||||
expect(html).toContain("Actualizar destinatario")
|
||||
expect(html).toContain('placeholder="Correo electrónico"')
|
||||
expect(html).toContain(">Ingeniería</option>")
|
||||
})
|
||||
|
||||
it("renders a localized edit-page not-found message", async () => {
|
||||
const { default: RecipientEditPage } = await import(
|
||||
"@/app/(dashboard)/recipients/[recipientId]/edit/page"
|
||||
)
|
||||
|
||||
mocks.findById.mockResolvedValue(null)
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
await RecipientEditPage({
|
||||
params: Promise.resolve({ recipientId: "missing-recipient" }),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain("Destinatario no encontrado")
|
||||
})
|
||||
|
||||
it("wires English recipient form submit copy through the new page", async () => {
|
||||
const { default: NewRecipientPage } = await import(
|
||||
"@/app/(dashboard)/recipients/new/page"
|
||||
)
|
||||
|
||||
mocks.getI18n.mockResolvedValueOnce({ dictionary: en, locale: "en" })
|
||||
|
||||
const html = renderToStaticMarkup(await NewRecipientPage())
|
||||
|
||||
expect(html).toContain("Create Recipient")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,165 @@
|
||||
import { createElement } from "react"
|
||||
import { renderToStaticMarkup } from "react-dom/server"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
import { es } from "@/i18n/dictionaries/es"
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
findAllPaginated: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
findAllByRecipient: vi.fn(),
|
||||
getI18n: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("@/i18n/server", () => ({
|
||||
getI18n: mocks.getI18n,
|
||||
}))
|
||||
|
||||
vi.mock("@/services/recipient.service", () => ({
|
||||
RecipientService: {
|
||||
findAllPaginated: mocks.findAllPaginated,
|
||||
findById: mocks.findById,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@/services/assignment.service", () => ({
|
||||
AssignmentService: {
|
||||
findAllByRecipient: mocks.findAllByRecipient,
|
||||
},
|
||||
}))
|
||||
|
||||
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("recipient pages localization", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" })
|
||||
})
|
||||
|
||||
it("renders the recipient list in Spanish while keeping stored department values display-only", async () => {
|
||||
const { default: RecipientsPage } = await import(
|
||||
"@/app/(dashboard)/recipients/page"
|
||||
)
|
||||
|
||||
mocks.findAllPaginated.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: "recipient-1",
|
||||
username: "ada",
|
||||
firstName: "Ada",
|
||||
lastName: "Lovelace",
|
||||
email: "ada@example.test",
|
||||
phone: "1234",
|
||||
department: "ENGINEERING",
|
||||
},
|
||||
],
|
||||
totalPages: 1,
|
||||
})
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
await RecipientsPage({ searchParams: Promise.resolve({}) }),
|
||||
)
|
||||
|
||||
expect(html).toContain("Destinatarios")
|
||||
expect(html).toContain("Agregar destinatario")
|
||||
expect(html).toContain("Usuario")
|
||||
expect(html).toContain("Nombre")
|
||||
expect(html).toContain("Correo electrónico")
|
||||
expect(html).toContain("Teléfono")
|
||||
expect(html).toContain("Departamento")
|
||||
expect(html).toContain("Acciones")
|
||||
expect(html).toContain("Ada Lovelace")
|
||||
expect(html).toContain("Ingeniería")
|
||||
expect(html).toContain('aria-label="Ver destinatario"')
|
||||
expect(html).toContain('aria-label="Editar destinatario"')
|
||||
expect(html).not.toContain(">ENGINEERING<")
|
||||
})
|
||||
|
||||
it("renders the localized recipient empty state when no recipients exist", async () => {
|
||||
const { default: RecipientsPage } = await import(
|
||||
"@/app/(dashboard)/recipients/page"
|
||||
)
|
||||
|
||||
mocks.findAllPaginated.mockResolvedValue({
|
||||
data: [],
|
||||
totalPages: 0,
|
||||
})
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
await RecipientsPage({ searchParams: Promise.resolve({}) }),
|
||||
)
|
||||
|
||||
expect(html).toContain("No se encontraron destinatarios.")
|
||||
})
|
||||
|
||||
it("renders localized recipient-owned detail labels and keeps assignments copy unchanged", async () => {
|
||||
const { default: RecipientInfoPage } = await import(
|
||||
"@/app/(dashboard)/recipients/[recipientId]/page"
|
||||
)
|
||||
|
||||
mocks.findById.mockResolvedValue({
|
||||
id: "recipient-1",
|
||||
username: "ada",
|
||||
firstName: "Ada",
|
||||
lastName: "Lovelace",
|
||||
email: "ada@example.test",
|
||||
phone: "1234",
|
||||
department: "DRIVER",
|
||||
})
|
||||
mocks.findAllByRecipient.mockResolvedValue([
|
||||
{
|
||||
id: "assignment-1",
|
||||
item: { name: "Laptop" },
|
||||
asset: { serialNumber: "SN-001" },
|
||||
quantity: 1,
|
||||
},
|
||||
])
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
await RecipientInfoPage({
|
||||
params: Promise.resolve({ recipientId: "recipient-1" }),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain("Usuario")
|
||||
expect(html).toContain("Correo electrónico")
|
||||
expect(html).toContain("Teléfono")
|
||||
expect(html).toContain("Departamento")
|
||||
expect(html).toContain("Chofer")
|
||||
expect(html).toContain("ada")
|
||||
expect(html).toContain("ada@example.test")
|
||||
expect(html).toContain("Assignments")
|
||||
expect(html).toContain("Laptop")
|
||||
expect(html).not.toContain(">DRIVER<")
|
||||
expect(html).not.toContain("Asignaciones")
|
||||
})
|
||||
|
||||
it("renders a localized recipient detail not-found message", async () => {
|
||||
const { default: RecipientInfoPage } = await import(
|
||||
"@/app/(dashboard)/recipients/[recipientId]/page"
|
||||
)
|
||||
|
||||
mocks.findById.mockResolvedValue(null)
|
||||
mocks.findAllByRecipient.mockResolvedValue([])
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
await RecipientInfoPage({
|
||||
params: Promise.resolve({ recipientId: "missing-recipient" }),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain("Destinatario no encontrado")
|
||||
})
|
||||
})
|
||||
@@ -559,6 +559,138 @@ describe("i18n dictionaries", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("provides localized recipient UI copy for English and Spanish", () => {
|
||||
expect(getDictionary("en").inventory.recipients).toEqual({
|
||||
list: {
|
||||
title: "Recipients",
|
||||
addLabel: "Add Recipient",
|
||||
empty: "No recipients found.",
|
||||
columns: {
|
||||
username: "Username",
|
||||
name: "Name",
|
||||
email: "Email",
|
||||
phone: "Phone",
|
||||
department: "Department",
|
||||
actions: "Actions",
|
||||
},
|
||||
actions: {
|
||||
view: "View recipient",
|
||||
edit: "Edit recipient",
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
notFound: "Recipient not found",
|
||||
labels: {
|
||||
username: "Username",
|
||||
email: "Email",
|
||||
phone: "Phone",
|
||||
department: "Department",
|
||||
},
|
||||
},
|
||||
new: {
|
||||
title: "Add Recipient",
|
||||
},
|
||||
edit: {
|
||||
title: "Edit Recipient",
|
||||
notFound: "Recipient not found",
|
||||
},
|
||||
form: {
|
||||
usernameLabel: "Username",
|
||||
usernamePlaceholder: "Username",
|
||||
firstNameLabel: "First Name",
|
||||
firstNamePlaceholder: "First name",
|
||||
lastNameLabel: "Last Name",
|
||||
lastNamePlaceholder: "Last name",
|
||||
departmentLabel: "Department",
|
||||
departmentPlaceholder: "Select a department",
|
||||
emailLabel: "Email",
|
||||
emailPlaceholder: "Email",
|
||||
phoneLabel: "Phone",
|
||||
phonePlaceholder: "Phone",
|
||||
createSubmit: "Create Recipient",
|
||||
updateSubmit: "Update Recipient",
|
||||
},
|
||||
fallback: {
|
||||
unknownDepartment: "Unknown department",
|
||||
},
|
||||
departments: {
|
||||
IT: "IT",
|
||||
ENGINEERING: "Engineering",
|
||||
LOGISTICS: "Logistics",
|
||||
TRAFFIC: "Traffic",
|
||||
DRIVER: "Driver",
|
||||
ADMINISTRATION: "Administration",
|
||||
SALES: "Sales",
|
||||
OTHER: "Other",
|
||||
},
|
||||
})
|
||||
|
||||
expect(getDictionary("es").inventory.recipients).toEqual({
|
||||
list: {
|
||||
title: "Destinatarios",
|
||||
addLabel: "Agregar destinatario",
|
||||
empty: "No se encontraron destinatarios.",
|
||||
columns: {
|
||||
username: "Usuario",
|
||||
name: "Nombre",
|
||||
email: "Correo electrónico",
|
||||
phone: "Teléfono",
|
||||
department: "Departamento",
|
||||
actions: "Acciones",
|
||||
},
|
||||
actions: {
|
||||
view: "Ver destinatario",
|
||||
edit: "Editar destinatario",
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
notFound: "Destinatario no encontrado",
|
||||
labels: {
|
||||
username: "Usuario",
|
||||
email: "Correo electrónico",
|
||||
phone: "Teléfono",
|
||||
department: "Departamento",
|
||||
},
|
||||
},
|
||||
new: {
|
||||
title: "Agregar destinatario",
|
||||
},
|
||||
edit: {
|
||||
title: "Editar destinatario",
|
||||
notFound: "Destinatario no encontrado",
|
||||
},
|
||||
form: {
|
||||
usernameLabel: "Usuario",
|
||||
usernamePlaceholder: "Usuario",
|
||||
firstNameLabel: "Nombre",
|
||||
firstNamePlaceholder: "Nombre",
|
||||
lastNameLabel: "Apellido",
|
||||
lastNamePlaceholder: "Apellido",
|
||||
departmentLabel: "Departamento",
|
||||
departmentPlaceholder: "Selecciona un departamento",
|
||||
emailLabel: "Correo electrónico",
|
||||
emailPlaceholder: "Correo electrónico",
|
||||
phoneLabel: "Teléfono",
|
||||
phonePlaceholder: "Teléfono",
|
||||
createSubmit: "Crear destinatario",
|
||||
updateSubmit: "Actualizar destinatario",
|
||||
},
|
||||
fallback: {
|
||||
unknownDepartment: "Departamento desconocido",
|
||||
},
|
||||
departments: {
|
||||
IT: "IT",
|
||||
ENGINEERING: "Ingeniería",
|
||||
LOGISTICS: "Logística",
|
||||
TRAFFIC: "Tráfico",
|
||||
DRIVER: "Chofer",
|
||||
ADMINISTRATION: "Administración",
|
||||
SALES: "Ventas",
|
||||
OTHER: "Otro",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("provides localized movement UI copy for English and Spanish", () => {
|
||||
expect(getDictionary("en").inventory.movements).toEqual({
|
||||
list: {
|
||||
|
||||
Reference in New Issue
Block a user