feat(i18n): localize inventory assignments UI

This commit is contained in:
2026-06-15 00:47:25 +02:00
parent 9b713c42e2
commit bfea2b77ab
13 changed files with 540 additions and 36 deletions
@@ -18,9 +18,10 @@ export default async function EditAssignmentPage({
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()
const copy = dictionary.inventory.assignments
if (!assignment) { if (!assignment) {
return <div>Assignment not found</div> return <div>{copy.edit.notFound}</div>
} }
let assignmentItem: Item = {} as Item let assignmentItem: Item = {} as Item
@@ -31,12 +32,16 @@ export default async function EditAssignmentPage({
} }
return ( return (
<div> <div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-4">
<h1 className="text-2xl font-bold">{copy.edit.title}</h1>
</div>
<AssignmentForm <AssignmentForm
recipients={recipients} recipients={recipients}
items={items} items={items}
assets={assets} assets={assets}
initialData={assignment as UpdateAssignmentFormType} initialData={assignment as UpdateAssignmentFormType}
formCopy={copy.form}
submitButtonCopy={dictionary.common.submitButton} submitButtonCopy={dictionary.common.submitButton}
/> />
</div> </div>
@@ -9,17 +9,21 @@ import {
SubmitButton, SubmitButton,
type SubmitButtonCopy, type SubmitButtonCopy,
} from "@/components/forms/submitButton" } from "@/components/forms/submitButton"
import type { Dictionary } from "@/i18n/dictionaries"
import { import {
type UpdateAssignmentFormType, type UpdateAssignmentFormType,
updateAssignmentSchema, updateAssignmentSchema,
} from "@/schemas/assignment.schema" } from "@/schemas/assignment.schema"
import type { Asset, Item, Recipient } from "@/types" import type { Asset, Item, Recipient } from "@/types"
type AssignmentFormCopy = Dictionary["inventory"]["assignments"]["form"]
interface Props { interface Props {
recipients: Recipient[] recipients: Recipient[]
items: Item[] items: Item[]
assets: Asset[] assets: Asset[]
initialData: UpdateAssignmentFormType initialData: UpdateAssignmentFormType
formCopy: AssignmentFormCopy
submitButtonCopy: SubmitButtonCopy submitButtonCopy: SubmitButtonCopy
} }
@@ -28,6 +32,7 @@ export default function EditAssignmentForm({
items, items,
assets, assets,
initialData, initialData,
formCopy,
submitButtonCopy, submitButtonCopy,
}: Props) { }: Props) {
const router = useRouter() const router = useRouter()
@@ -71,7 +76,7 @@ export default function EditAssignmentForm({
<input type="hidden" {...register("id")} /> <input type="hidden" {...register("id")} />
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label htmlFor="recipientId" className="mb-2 block text-lg"> <label htmlFor="recipientId" className="mb-2 block text-lg">
Recipient {formCopy.recipientLabel}
</label> </label>
<select <select
id="recipientId" id="recipientId"
@@ -92,7 +97,7 @@ export default function EditAssignmentForm({
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label htmlFor="itemId" className="mb-2 block text-lg"> <label htmlFor="itemId" className="mb-2 block text-lg">
Item {formCopy.itemLabel}
</label> </label>
<select <select
id="itemId" id="itemId"
@@ -111,7 +116,7 @@ export default function EditAssignmentForm({
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label htmlFor="assetId" className="mb-2 block text-lg"> <label htmlFor="assetId" className="mb-2 block text-lg">
Asset {formCopy.assetLabel}
</label> </label>
<select <select
id="assetId" id="assetId"
@@ -120,7 +125,7 @@ export default function EditAssignmentForm({
errors.assetId ? "border-error" : "" errors.assetId ? "border-error" : ""
}`} }`}
> >
<option value="">Select an asset</option> <option value="">{formCopy.assetPlaceholder}</option>
{itemId {itemId
? assets.map((asset) => ( ? assets.map((asset) => (
<option key={asset.id} value={asset.id}> <option key={asset.id} value={asset.id}>
@@ -135,7 +140,7 @@ export default function EditAssignmentForm({
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label htmlFor="quantity" className="mb-2 block text-lg"> <label htmlFor="quantity" className="mb-2 block text-lg">
Quantity {formCopy.quantityLabel}
</label> </label>
<input <input
type="number" type="number"
@@ -143,6 +148,7 @@ export default function EditAssignmentForm({
disabled={!itemId || assets.length > 0} disabled={!itemId || assets.length > 0}
min={1} min={1}
max={itemId ? items.find((item) => item.id === itemId)?.stock : 0} max={itemId ? items.find((item) => item.id === itemId)?.stock : 0}
placeholder={formCopy.quantityPlaceholder}
defaultValue={1} defaultValue={1}
{...register("quantity")} {...register("quantity")}
className={`w-full rounded-lg border px-4 py-2 ${ className={`w-full rounded-lg border px-4 py-2 ${
@@ -159,7 +165,7 @@ export default function EditAssignmentForm({
isSubmitSuccessful={isSubmitSuccessful} isSubmitSuccessful={isSubmitSuccessful}
disabled={!itemId || (assets.length > 0 && !assetId)} disabled={!itemId || (assets.length > 0 && !assetId)}
> >
Update Assignment {formCopy.updateSubmit}
</SubmitButton> </SubmitButton>
</form> </form>
) )
@@ -10,16 +10,20 @@ import {
SubmitButton, SubmitButton,
type SubmitButtonCopy, type SubmitButtonCopy,
} from "@/components/forms/submitButton" } from "@/components/forms/submitButton"
import type { Dictionary } from "@/i18n/dictionaries"
import { import {
type CreateAssignmentFormType, type CreateAssignmentFormType,
createAssignmentSchema, createAssignmentSchema,
} from "@/schemas/assignment.schema" } from "@/schemas/assignment.schema"
import type { Asset, Item, Recipient } from "@/types" import type { Asset, Item, Recipient } from "@/types"
type AssignmentFormCopy = Dictionary["inventory"]["assignments"]["form"]
interface Props { interface Props {
recipients: Recipient[] recipients: Recipient[]
items: Item[] items: Item[]
assets: Asset[] assets: Asset[]
formCopy: AssignmentFormCopy
submitButtonCopy: SubmitButtonCopy submitButtonCopy: SubmitButtonCopy
} }
@@ -27,6 +31,7 @@ export default function CreateAssignmentForm({
recipients, recipients,
items, items,
assets, assets,
formCopy,
submitButtonCopy, submitButtonCopy,
}: Props) { }: Props) {
const router = useRouter() const router = useRouter()
@@ -69,7 +74,7 @@ export default function CreateAssignmentForm({
<form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}> <form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label htmlFor="recipientId" className="mb-2 block text-lg"> <label htmlFor="recipientId" className="mb-2 block text-lg">
Recipient {formCopy.recipientLabel}
</label> </label>
<select <select
id="recipientId" id="recipientId"
@@ -78,7 +83,7 @@ export default function CreateAssignmentForm({
errors.recipientId ? "border-error" : "" errors.recipientId ? "border-error" : ""
}`} }`}
> >
<option value="">Select a recipient</option> <option value="">{formCopy.recipientPlaceholder}</option>
{recipients.map((recipient) => ( {recipients.map((recipient) => (
<option key={recipient.id} value={recipient.id}> <option key={recipient.id} value={recipient.id}>
{recipient.firstName} {recipient.lastName} {recipient.firstName} {recipient.lastName}
@@ -91,7 +96,7 @@ export default function CreateAssignmentForm({
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label htmlFor="itemId" className="mb-2 block text-lg"> <label htmlFor="itemId" className="mb-2 block text-lg">
Item {formCopy.itemLabel}
</label> </label>
<select <select
id="itemId" id="itemId"
@@ -100,7 +105,7 @@ export default function CreateAssignmentForm({
errors.itemId ? "border-error" : "" errors.itemId ? "border-error" : ""
}`} }`}
> >
<option value="">Select an item</option> <option value="">{formCopy.itemPlaceholder}</option>
{items.map((item) => ( {items.map((item) => (
<option key={item.id} value={item.id}> <option key={item.id} value={item.id}>
{item.name} {item.name}
@@ -112,7 +117,7 @@ export default function CreateAssignmentForm({
{itemId && itemAssets.length !== 0 && ( {itemId && itemAssets.length !== 0 && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label htmlFor="assetId" className="mb-2 block text-lg"> <label htmlFor="assetId" className="mb-2 block text-lg">
Asset {formCopy.assetLabel}
</label> </label>
<select <select
id="assetId" id="assetId"
@@ -124,7 +129,7 @@ export default function CreateAssignmentForm({
: "" : ""
}`} }`}
> >
<option value="">Select an asset</option> <option value="">{formCopy.assetPlaceholder}</option>
{itemId {itemId
? itemAssets.map((asset) => ( ? itemAssets.map((asset) => (
<option key={asset.id} value={asset.id}> <option key={asset.id} value={asset.id}>
@@ -140,7 +145,7 @@ export default function CreateAssignmentForm({
)} )}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label htmlFor="quantity" className="mb-2 block text-lg"> <label htmlFor="quantity" className="mb-2 block text-lg">
Quantity {formCopy.quantityLabel}
</label> </label>
<input <input
type="number" type="number"
@@ -148,6 +153,7 @@ export default function CreateAssignmentForm({
disabled={!itemId || itemAssets.length > 0} disabled={!itemId || itemAssets.length > 0}
min={1} min={1}
max={itemId ? items.find((item) => item.id === itemId)?.stock : 0} max={itemId ? items.find((item) => item.id === itemId)?.stock : 0}
placeholder={formCopy.quantityPlaceholder}
defaultValue={1} defaultValue={1}
{...register("quantity")} {...register("quantity")}
className={`w-full rounded-lg border px-4 py-2 ${ className={`w-full rounded-lg border px-4 py-2 ${
@@ -166,7 +172,7 @@ export default function CreateAssignmentForm({
isSubmitSuccessful={isSubmitSuccessful} isSubmitSuccessful={isSubmitSuccessful}
disabled={!itemId || (itemAssets.length > 0 && !assetId)} disabled={!itemId || (itemAssets.length > 0 && !assetId)}
> >
Create Assignment {formCopy.createSubmit}
</SubmitButton> </SubmitButton>
</form> </form>
) )
@@ -10,8 +10,10 @@ import type { ReturnAssignmentFormType } from "@/schemas/assignment.schema"
export default function ReturnButton({ export default function ReturnButton({
assignmentId, assignmentId,
ariaLabel,
}: { }: {
assignmentId: string assignmentId: string
ariaLabel: string
}) { }) {
const router = useRouter() const router = useRouter()
const [isPending, startTransition] = useTransition() const [isPending, startTransition] = useTransition()
@@ -42,6 +44,7 @@ export default function ReturnButton({
className="btn btn-error" className="btn btn-error"
size="icon" size="icon"
variant="outline" variant="outline"
aria-label={ariaLabel}
disabled={isPending} disabled={isPending}
> >
<ArrowLeft /> <ArrowLeft />
+13 -6
View File
@@ -10,13 +10,20 @@ export default async function NewAssignmentPage() {
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()
const copy = dictionary.inventory.assignments
return ( return (
<AssignmentForm <div className="flex flex-col gap-4">
recipients={recipients} <div className="flex items-center justify-between gap-4">
items={items} <h1 className="text-2xl font-bold">{copy.new.title}</h1>
assets={assets} </div>
submitButtonCopy={dictionary.common.submitButton} <AssignmentForm
/> recipients={recipients}
items={items}
assets={assets}
formCopy={copy.form}
submitButtonCopy={dictionary.common.submitButton}
/>
</div>
) )
} }
+21 -10
View File
@@ -4,6 +4,7 @@ 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 { getI18n } from "@/i18n/server"
import { AssignmentService } from "@/services/assignment.service" import { AssignmentService } from "@/services/assignment.service"
import ReturnButton from "./_components/return.button" import ReturnButton from "./_components/return.button"
@@ -22,35 +23,38 @@ export default async function AssignmentsPage(props: {
page: currentPage, page: currentPage,
search, search,
}) })
const { dictionary } = await getI18n()
const copy = dictionary.inventory.assignments
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<PageHeader <PageHeader
title="Assignments" title={copy.list.title}
link="/assignments/new" link="/assignments/new"
search={search} search={search}
data={assignments} data={assignments}
addLabel={copy.list.addLabel}
/> />
{assignments.length === 0 && <div>No assignments found</div>} {assignments.length === 0 && <div>{copy.list.empty}</div>}
{assignments.length > 0 && ( {assignments.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"> <th scope="col" className="p-4">
Recipient {copy.list.columns.recipient}
</th> </th>
<th scope="col" className="p-4"> <th scope="col" className="p-4">
Item {copy.list.columns.item}
</th> </th>
<th scope="col" className="p-4"> <th scope="col" className="p-4">
Serial Number {copy.list.columns.serialNumber}
</th> </th>
<th scope="col" className="p-4"> <th scope="col" className="p-4">
Quantity {copy.list.columns.quantity}
</th> </th>
<th scope="col" className="p-4"> <th scope="col" className="p-4">
Actions {copy.list.columns.actions}
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -75,7 +79,8 @@ export default async function AssignmentsPage(props: {
</Link> </Link>
</td> </td>
<td className="p-4"> <td className="p-4">
{assignment?.asset?.serialNumber || "N/A"} {assignment?.asset?.serialNumber ||
copy.fallback.missingValue}
</td> </td>
<td className="p-4">{assignment?.quantity}</td> <td className="p-4">{assignment?.quantity}</td>
<td className="p-4"> <td className="p-4">
@@ -84,11 +89,17 @@ export default async function AssignmentsPage(props: {
href={`/assignments/${assignment.id}/edit`} href={`/assignments/${assignment.id}/edit`}
passHref passHref
> >
<Button variant="outline"> <Button
variant="outline"
aria-label={copy.list.actions.edit}
>
<Pencil /> <Pencil />
</Button> </Button>
</Link> </Link>
<ReturnButton assignmentId={assignment.id} /> <ReturnButton
assignmentId={assignment.id}
ariaLabel={copy.list.actions.return}
/>
</div> </div>
</td> </td>
</tr> </tr>
@@ -13,6 +13,7 @@ export default async function RecipientInfoPage({
const { recipientId } = await params const { recipientId } = await params
const { dictionary } = await getI18n() const { dictionary } = await getI18n()
const copy = dictionary.inventory.recipients const copy = dictionary.inventory.recipients
const assignmentCopy = dictionary.inventory.assignments
const recipient = await RecipientService.findById(recipientId) const recipient = await RecipientService.findById(recipientId)
const assignments = await AssignmentService.findAllByRecipient(recipientId) const assignments = await AssignmentService.findAllByRecipient(recipientId)
@@ -62,7 +63,7 @@ export default async function RecipientInfoPage({
{assignments.length > 0 && ( {assignments.length > 0 && (
<Card className="rounded-sm shadow-none"> <Card className="rounded-sm shadow-none">
<CardHeader> <CardHeader>
<CardTitle>Assignments</CardTitle> <CardTitle>{assignmentCopy.list.title}</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex flex-col gap-y-2 text-sm"> <div className="flex flex-col gap-y-2 text-sm">
+40
View File
@@ -255,6 +255,46 @@ export const en = {
invalidUpdateStatus: "Invalid status", invalidUpdateStatus: "Invalid status",
}, },
}, },
assignments: {
list: {
title: "Assignments",
addLabel: "Add Assignment",
empty: "No assignments found.",
columns: {
recipient: "Recipient",
item: "Item",
serialNumber: "Serial Number",
quantity: "Quantity",
actions: "Actions",
},
actions: {
edit: "Edit assignment",
return: "Return assignment",
},
},
new: {
title: "New Assignment",
},
edit: {
title: "Edit Assignment",
notFound: "Assignment not found",
},
form: {
recipientLabel: "Recipient",
recipientPlaceholder: "Select a recipient",
itemLabel: "Item",
itemPlaceholder: "Select an item",
assetLabel: "Asset",
assetPlaceholder: "Select an asset",
quantityLabel: "Quantity",
quantityPlaceholder: "1",
createSubmit: "Create Assignment",
updateSubmit: "Update Assignment",
},
fallback: {
missingValue: "N/A",
},
},
recipients: { recipients: {
list: { list: {
title: "Recipients", title: "Recipients",
+40
View File
@@ -259,6 +259,46 @@ export const es = {
invalidUpdateStatus: "Estado inválido", invalidUpdateStatus: "Estado inválido",
}, },
}, },
assignments: {
list: {
title: "Asignaciones",
addLabel: "Agregar asignación",
empty: "No se encontraron asignaciones.",
columns: {
recipient: "Destinatario",
item: "Artículo",
serialNumber: "Número de serie",
quantity: "Cantidad",
actions: "Acciones",
},
actions: {
edit: "Editar asignación",
return: "Devolver asignación",
},
},
new: {
title: "Nueva asignación",
},
edit: {
title: "Editar asignación",
notFound: "Asignación no encontrada",
},
form: {
recipientLabel: "Destinatario",
recipientPlaceholder: "Selecciona un destinatario",
itemLabel: "Artículo",
itemPlaceholder: "Selecciona un artículo",
assetLabel: "Activo",
assetPlaceholder: "Selecciona un activo",
quantityLabel: "Cantidad",
quantityPlaceholder: "1",
createSubmit: "Crear asignación",
updateSubmit: "Actualizar asignación",
},
fallback: {
missingValue: "No disponible",
},
},
recipients: { recipients: {
list: { list: {
title: "Destinatarios", title: "Destinatarios",
@@ -0,0 +1,177 @@
import { renderToStaticMarkup } from "react-dom/server"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { es } from "@/i18n/dictionaries/es"
const mocks = vi.hoisted(() => ({
getI18n: vi.fn(),
findAllRecipients: vi.fn(),
findAllItemsWithStock: vi.fn(),
findAllAvailableAssets: vi.fn(),
findAllItems: vi.fn(),
findItemById: vi.fn(),
findAllAssets: vi.fn(),
findAssignmentById: vi.fn(),
createAssignment: vi.fn(),
updateAssignment: vi.fn(),
push: vi.fn(),
toastError: vi.fn(),
toastSuccess: vi.fn(),
}))
vi.mock("@/i18n/server", () => ({
getI18n: mocks.getI18n,
}))
vi.mock("@/services/recipient.service", () => ({
RecipientService: {
findAll: mocks.findAllRecipients,
},
}))
vi.mock("@/services/item.service", () => ({
ItemService: {
findAllWithStock: mocks.findAllItemsWithStock,
findAll: mocks.findAllItems,
findById: mocks.findItemById,
},
}))
vi.mock("@/services/asset.service", () => ({
AssetService: {
findAllAvailable: mocks.findAllAvailableAssets,
findAll: mocks.findAllAssets,
},
}))
vi.mock("@/services/assignment.service", () => ({
AssignmentService: {
findById: mocks.findAssignmentById,
},
}))
vi.mock("@/actions/assignment.actions", () => ({
createAssignment: mocks.createAssignment,
updateAssignment: mocks.updateAssignment,
}))
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: mocks.push,
}),
}))
vi.mock("sonner", () => ({
toast: {
error: mocks.toastError,
success: mocks.toastSuccess,
},
}))
describe("assignment form pages localization", () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" })
mocks.findAllRecipients.mockResolvedValue([
{
id: "recipient-1",
firstName: "Ada",
lastName: "Lovelace",
},
])
mocks.findAllItemsWithStock.mockResolvedValue([
{
id: "item-1",
name: "Laptop",
stock: 5,
},
])
mocks.findAllItems.mockResolvedValue([
{
id: "item-1",
name: "Laptop",
stock: 5,
},
])
mocks.findAllAvailableAssets.mockResolvedValue([
{
id: "asset-1",
itemId: "item-1",
serialNumber: "SN-001",
},
])
mocks.findAllAssets.mockResolvedValue([
{
id: "asset-1",
itemId: "item-1",
serialNumber: "SN-001",
},
])
mocks.findItemById.mockResolvedValue({
id: "item-1",
name: "Laptop",
stock: 5,
})
})
it("renders the new assignment page with localized heading and form copy", async () => {
const { default: NewAssignmentPage } = await import(
"@/app/(dashboard)/assignments/new/page"
)
const html = renderToStaticMarkup(await NewAssignmentPage())
expect(html).toContain("Nueva asignación")
expect(html).toContain("Destinatario")
expect(html).toContain(
'option value="">Selecciona un destinatario</option>',
)
expect(html).toContain("Artículo")
expect(html).toContain('option value="">Selecciona un artículo</option>')
expect(html).toContain("Cantidad")
expect(html).toContain('placeholder="1"')
expect(html).toContain("Crear asignación")
expect(html).toContain("Ada Lovelace")
expect(html).toContain("Laptop")
})
it("renders the edit assignment page with localized heading, not-found copy, and submit text", async () => {
const { default: EditAssignmentPage } = await import(
"@/app/(dashboard)/assignments/[assignmentId]/edit/page"
)
mocks.findAssignmentById.mockResolvedValue({
id: "assignment-1",
recipientId: "recipient-1",
itemId: "item-1",
assetId: "asset-1",
quantity: 1,
})
const html = renderToStaticMarkup(
await EditAssignmentPage({
params: Promise.resolve({ assignmentId: "assignment-1" }),
}),
)
expect(html).toContain("Editar asignación")
expect(html).toContain("Destinatario")
expect(html).toContain("Artículo")
expect(html).toContain("Activo")
expect(html).toContain('option value="">Selecciona un activo</option>')
expect(html).toContain("Actualizar asignación")
expect(html).toContain("Ada Lovelace")
expect(html).toContain("Laptop")
expect(html).toContain("SN-001")
mocks.findAssignmentById.mockResolvedValueOnce(null)
const notFoundHtml = renderToStaticMarkup(
await EditAssignmentPage({
params: Promise.resolve({ assignmentId: "missing-assignment" }),
}),
)
expect(notFoundHtml).toContain("Asignación no encontrada")
})
})
@@ -0,0 +1,124 @@
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(() => ({
findAllWithRecipientPaginated: vi.fn(),
getI18n: vi.fn(),
refresh: vi.fn(),
returnAssignment: vi.fn(),
toastError: vi.fn(),
toastSuccess: vi.fn(),
}))
vi.mock("@/i18n/server", () => ({
getI18n: mocks.getI18n,
}))
vi.mock("@/services/assignment.service", () => ({
AssignmentService: {
findAllWithRecipientPaginated: mocks.findAllWithRecipientPaginated,
},
}))
vi.mock("@/actions/assignment.actions", () => ({
returnAssignment: mocks.returnAssignment,
}))
vi.mock("next/navigation", () => ({
useRouter: () => ({
refresh: mocks.refresh,
}),
}))
vi.mock("sonner", () => ({
toast: {
error: mocks.toastError,
success: mocks.toastSuccess,
},
}))
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("assignment pages localization", () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" })
})
it("renders the assignment list in Spanish with localized action labels and unchanged assignment data", async () => {
const { default: AssignmentsPage } = await import(
"@/app/(dashboard)/assignments/page"
)
mocks.findAllWithRecipientPaginated.mockResolvedValue({
data: [
{
id: "assignment-1",
quantity: 2,
recipient: {
id: "recipient-1",
firstName: "Ada",
lastName: "Lovelace",
},
item: {
id: "item-1",
name: "Laptop",
},
asset: {
serialNumber: null,
},
},
],
totalPages: 1,
})
const html = renderToStaticMarkup(
await AssignmentsPage({ searchParams: Promise.resolve({}) }),
)
expect(html).toContain("Asignaciones")
expect(html).toContain("Agregar asignación")
expect(html).toContain("Destinatario")
expect(html).toContain("Artículo")
expect(html).toContain("Número de serie")
expect(html).toContain("Cantidad")
expect(html).toContain("Acciones")
expect(html).toContain("Ada Lovelace")
expect(html).toContain("Laptop")
expect(html).toContain("No disponible")
expect(html).toContain('aria-label="Editar asignación"')
expect(html).toContain('aria-label="Devolver asignación"')
})
it("renders the localized assignment empty state when no assignments exist", async () => {
const { default: AssignmentsPage } = await import(
"@/app/(dashboard)/assignments/page"
)
mocks.findAllWithRecipientPaginated.mockResolvedValue({
data: [],
totalPages: 0,
})
const html = renderToStaticMarkup(
await AssignmentsPage({ searchParams: Promise.resolve({}) }),
)
expect(html).toContain("No se encontraron asignaciones.")
})
})
@@ -104,7 +104,7 @@ describe("recipient pages localization", () => {
expect(html).toContain("No se encontraron destinatarios.") expect(html).toContain("No se encontraron destinatarios.")
}) })
it("renders localized recipient-owned detail labels and keeps assignments copy unchanged", async () => { it("renders localized recipient-owned detail labels and localizes the embedded assignments title only", async () => {
const { default: RecipientInfoPage } = await import( const { default: RecipientInfoPage } = await import(
"@/app/(dashboard)/recipients/[recipientId]/page" "@/app/(dashboard)/recipients/[recipientId]/page"
) )
@@ -140,10 +140,10 @@ describe("recipient pages localization", () => {
expect(html).toContain("Chofer") expect(html).toContain("Chofer")
expect(html).toContain("ada") expect(html).toContain("ada")
expect(html).toContain("ada@example.test") expect(html).toContain("ada@example.test")
expect(html).toContain("Assignments") expect(html).toContain("Asignaciones")
expect(html).toContain("Laptop") expect(html).toContain("Laptop")
expect(html).not.toContain(">DRIVER<") expect(html).not.toContain(">DRIVER<")
expect(html).not.toContain("Asignaciones") expect(html).not.toContain("Assignments")
}) })
it("renders a localized recipient detail not-found message", async () => { it("renders a localized recipient detail not-found message", async () => {
+84
View File
@@ -408,6 +408,90 @@ describe("i18n dictionaries", () => {
}) })
}) })
it("provides localized assignment copy for English and Spanish", () => {
expect(getDictionary("en").inventory.assignments).toEqual({
list: {
title: "Assignments",
addLabel: "Add Assignment",
empty: "No assignments found.",
columns: {
recipient: "Recipient",
item: "Item",
serialNumber: "Serial Number",
quantity: "Quantity",
actions: "Actions",
},
actions: {
edit: "Edit assignment",
return: "Return assignment",
},
},
new: {
title: "New Assignment",
},
edit: {
title: "Edit Assignment",
notFound: "Assignment not found",
},
form: {
recipientLabel: "Recipient",
recipientPlaceholder: "Select a recipient",
itemLabel: "Item",
itemPlaceholder: "Select an item",
assetLabel: "Asset",
assetPlaceholder: "Select an asset",
quantityLabel: "Quantity",
quantityPlaceholder: "1",
createSubmit: "Create Assignment",
updateSubmit: "Update Assignment",
},
fallback: {
missingValue: "N/A",
},
})
expect(getDictionary("es").inventory.assignments).toEqual({
list: {
title: "Asignaciones",
addLabel: "Agregar asignación",
empty: "No se encontraron asignaciones.",
columns: {
recipient: "Destinatario",
item: "Artículo",
serialNumber: "Número de serie",
quantity: "Cantidad",
actions: "Acciones",
},
actions: {
edit: "Editar asignación",
return: "Devolver asignación",
},
},
new: {
title: "Nueva asignación",
},
edit: {
title: "Editar asignación",
notFound: "Asignación no encontrada",
},
form: {
recipientLabel: "Destinatario",
recipientPlaceholder: "Selecciona un destinatario",
itemLabel: "Artículo",
itemPlaceholder: "Selecciona un artículo",
assetLabel: "Activo",
assetPlaceholder: "Selecciona un activo",
quantityLabel: "Cantidad",
quantityPlaceholder: "1",
createSubmit: "Crear asignación",
updateSubmit: "Actualizar asignación",
},
fallback: {
missingValue: "No disponible",
},
})
})
it("provides localized inventory asset UI copy for English and Spanish", () => { it("provides localized inventory asset UI copy for English and Spanish", () => {
expect(getDictionary("en").inventory.assets).toEqual({ expect(getDictionary("en").inventory.assets).toEqual({
list: { list: {