feat(i18n): localize inventory assignments UI
This commit is contained in:
@@ -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 />
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<h1 className="text-2xl font-bold">{copy.new.title}</h1>
|
||||||
|
</div>
|
||||||
<AssignmentForm
|
<AssignmentForm
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
items={items}
|
items={items}
|
||||||
assets={assets}
|
assets={assets}
|
||||||
|
formCopy={copy.form}
|
||||||
submitButtonCopy={dictionary.common.submitButton}
|
submitButtonCopy={dictionary.common.submitButton}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user