feat(i18n): localize inventory items UI
This commit is contained in:
@@ -13,24 +13,26 @@ export default async function AddItem({
|
|||||||
const categories = await CategoryService.findAll()
|
const categories = await CategoryService.findAll()
|
||||||
const item = await ItemService.findByIdWithAssetCount(itemId)
|
const item = await ItemService.findByIdWithAssetCount(itemId)
|
||||||
const { dictionary } = await getI18n()
|
const { dictionary } = await getI18n()
|
||||||
|
const copy = dictionary.inventory.items
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return <div>Item not found</div>
|
return <div>{copy.edit.notFound}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{item?._count?.assets && item?._count.assets > 0 && (
|
{item?._count?.assets && item?._count.assets > 0 && (
|
||||||
<div className="rounded-sm bg-red-100 p-4 text-red-800">
|
<div className="rounded-sm bg-red-100 p-4 text-red-800">
|
||||||
<p>{`This item has already assets assigned to it.`}</p>
|
<p>{copy.edit.hasAssetsWarning}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<h1 className="text-2xl font-bold">Edit Item</h1>
|
<h1 className="text-2xl font-bold">{copy.edit.title}</h1>
|
||||||
</div>
|
</div>
|
||||||
<UpdateItemForm
|
<UpdateItemForm
|
||||||
categories={categories}
|
categories={categories}
|
||||||
item={item}
|
item={item}
|
||||||
|
formCopy={copy.form}
|
||||||
submitButtonCopy={dictionary.common.submitButton}
|
submitButtonCopy={dictionary.common.submitButton}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { getI18n } from "@/i18n/server"
|
||||||
import { AssetService } from "@/services/asset.service"
|
import { AssetService } from "@/services/asset.service"
|
||||||
import { ItemService } from "@/services/item.service"
|
import { ItemService } from "@/services/item.service"
|
||||||
import { MovementService } from "@/services/movement.service"
|
import { MovementService } from "@/services/movement.service"
|
||||||
@@ -12,9 +13,11 @@ export default async function ItemPage({
|
|||||||
const item = await ItemService.findByIdWithCategory(itemId)
|
const item = await ItemService.findByIdWithCategory(itemId)
|
||||||
const assets = await AssetService.findByItemId(itemId)
|
const assets = await AssetService.findByItemId(itemId)
|
||||||
const movements = await MovementService.findAllByItemId(itemId)
|
const movements = await MovementService.findAllByItemId(itemId)
|
||||||
|
const { dictionary } = await getI18n()
|
||||||
|
const copy = dictionary.inventory.items.detail
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return <div>Item not found</div>
|
return <div>{copy.notFound}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -26,11 +29,11 @@ export default async function ItemPage({
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-sm">
|
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-sm">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">Category</span>
|
<span className="text-gray-600">{copy.labels.category}</span>
|
||||||
<span>{item.category.name}</span>
|
<span>{item.category.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">Stock</span>
|
<span className="text-gray-600">{copy.labels.stock}</span>
|
||||||
<span>{item.stock}</span>
|
<span>{item.stock}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,15 @@ import { toast } from "sonner"
|
|||||||
import { deleteItemAction } from "@/actions/item.actions"
|
import { deleteItemAction } from "@/actions/item.actions"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
export default function DeleteItemButton({ itemId }: { itemId: string }) {
|
import type { ItemDeleteCopy } from "./item.copy"
|
||||||
|
|
||||||
|
export default function DeleteItemButton({
|
||||||
|
itemId,
|
||||||
|
copy,
|
||||||
|
}: {
|
||||||
|
itemId: string
|
||||||
|
copy: ItemDeleteCopy
|
||||||
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [isPending, startTransition] = useTransition()
|
const [isPending, startTransition] = useTransition()
|
||||||
|
|
||||||
@@ -24,7 +32,7 @@ export default function DeleteItemButton({ itemId }: { itemId: string }) {
|
|||||||
toast.success(response.message)
|
toast.success(response.message)
|
||||||
router.refresh()
|
router.refresh()
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.message ?? "Unknown error")
|
toast.error(response.message ?? copy.unknownError)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -38,6 +46,7 @@ export default function DeleteItemButton({ itemId }: { itemId: string }) {
|
|||||||
size="icon"
|
size="icon"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
|
aria-label={isPending ? copy.pending : copy.label}
|
||||||
>
|
>
|
||||||
<Trash />
|
<Trash />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Dictionary } from "@/i18n/dictionaries"
|
||||||
|
|
||||||
|
export type ItemListCopy = Dictionary["inventory"]["items"]["list"]
|
||||||
|
export type ItemDetailCopy = Dictionary["inventory"]["items"]["detail"]
|
||||||
|
export type ItemFormCopy = Dictionary["inventory"]["items"]["form"]
|
||||||
|
export type ItemDeleteCopy = Dictionary["inventory"]["items"]["delete"]
|
||||||
@@ -15,11 +15,15 @@ import {
|
|||||||
} from "@/schemas/item.schema"
|
} from "@/schemas/item.schema"
|
||||||
import type { CategorySummary } from "@/types"
|
import type { CategorySummary } from "@/types"
|
||||||
|
|
||||||
|
import type { ItemFormCopy } from "./item.copy"
|
||||||
|
|
||||||
export default function NewItemForm({
|
export default function NewItemForm({
|
||||||
categories,
|
categories,
|
||||||
|
formCopy,
|
||||||
submitButtonCopy,
|
submitButtonCopy,
|
||||||
}: {
|
}: {
|
||||||
categories: CategorySummary[]
|
categories: CategorySummary[]
|
||||||
|
formCopy: ItemFormCopy
|
||||||
submitButtonCopy: SubmitButtonCopy
|
submitButtonCopy: SubmitButtonCopy
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -61,12 +65,12 @@ export default function NewItemForm({
|
|||||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
|
<form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="name" className="mb-2 block text-lg">
|
<label htmlFor="name" className="mb-2 block text-lg">
|
||||||
Name
|
{formCopy.nameLabel}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="name"
|
id="name"
|
||||||
placeholder="Item name"
|
placeholder={formCopy.namePlaceholder}
|
||||||
{...register("name")}
|
{...register("name")}
|
||||||
className="w-full rounded-lg border px-4 py-2"
|
className="w-full rounded-lg border px-4 py-2"
|
||||||
/>
|
/>
|
||||||
@@ -74,14 +78,14 @@ export default function NewItemForm({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="categoryId" className="mb-2 block text-lg">
|
<label htmlFor="categoryId" className="mb-2 block text-lg">
|
||||||
Category
|
{formCopy.categoryLabel}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="categoryId"
|
id="categoryId"
|
||||||
{...register("categoryId")}
|
{...register("categoryId")}
|
||||||
className="w-full rounded-lg border px-4 py-2"
|
className="w-full rounded-lg border px-4 py-2"
|
||||||
>
|
>
|
||||||
<option value="">Select a category</option>
|
<option value="">{formCopy.categoryPlaceholder}</option>
|
||||||
{categories?.map((category) => (
|
{categories?.map((category) => (
|
||||||
<option key={category.id} value={category.id}>
|
<option key={category.id} value={category.id}>
|
||||||
{category.name}
|
{category.name}
|
||||||
@@ -94,13 +98,13 @@ export default function NewItemForm({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="stock" className="mb-2 block text-lg">
|
<label htmlFor="stock" className="mb-2 block text-lg">
|
||||||
Stock
|
{formCopy.stockLabel}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="stock"
|
id="stock"
|
||||||
pattern="{[0-9]*}"
|
pattern="{[0-9]*}"
|
||||||
placeholder="0"
|
placeholder={formCopy.stockPlaceholder}
|
||||||
min="0"
|
min="0"
|
||||||
{...register("stock")}
|
{...register("stock")}
|
||||||
className="w-full rounded-lg border px-4 py-2"
|
className="w-full rounded-lg border px-4 py-2"
|
||||||
@@ -124,7 +128,7 @@ export default function NewItemForm({
|
|||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
isSubmitSuccessful={isSubmitSuccessful}
|
isSubmitSuccessful={isSubmitSuccessful}
|
||||||
>
|
>
|
||||||
Create Item
|
{formCopy.createSubmit}
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,13 +15,17 @@ import {
|
|||||||
} from "@/schemas/item.schema"
|
} from "@/schemas/item.schema"
|
||||||
import type { CategorySummary, ItemWithAssetCount } from "@/types"
|
import type { CategorySummary, ItemWithAssetCount } from "@/types"
|
||||||
|
|
||||||
|
import type { ItemFormCopy } from "./item.copy"
|
||||||
|
|
||||||
export default function UpdateItemForm({
|
export default function UpdateItemForm({
|
||||||
categories,
|
categories,
|
||||||
item,
|
item,
|
||||||
|
formCopy,
|
||||||
submitButtonCopy,
|
submitButtonCopy,
|
||||||
}: {
|
}: {
|
||||||
categories: CategorySummary[]
|
categories: CategorySummary[]
|
||||||
item: ItemWithAssetCount
|
item: ItemWithAssetCount
|
||||||
|
formCopy: ItemFormCopy
|
||||||
submitButtonCopy: SubmitButtonCopy
|
submitButtonCopy: SubmitButtonCopy
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -72,12 +76,12 @@ export default function UpdateItemForm({
|
|||||||
{item?.id && <input type="hidden" name="id" value={item.id} />}
|
{item?.id && <input type="hidden" name="id" value={item.id} />}
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="name" className="mb-2 block text-lg">
|
<label htmlFor="name" className="mb-2 block text-lg">
|
||||||
Name
|
{formCopy.nameLabel}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="name"
|
id="name"
|
||||||
placeholder="Item name"
|
placeholder={formCopy.namePlaceholder}
|
||||||
{...register("name")}
|
{...register("name")}
|
||||||
className={`w-full rounded-lg border px-4 py-2`}
|
className={`w-full rounded-lg border px-4 py-2`}
|
||||||
/>
|
/>
|
||||||
@@ -85,7 +89,7 @@ export default function UpdateItemForm({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="categoryId" className="mb-2 block text-lg">
|
<label htmlFor="categoryId" className="mb-2 block text-lg">
|
||||||
Category
|
{formCopy.categoryLabel}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="categoryId"
|
id="categoryId"
|
||||||
@@ -93,7 +97,7 @@ export default function UpdateItemForm({
|
|||||||
{...register("categoryId")}
|
{...register("categoryId")}
|
||||||
className={`w-full rounded-lg border px-4 py-2`}
|
className={`w-full rounded-lg border px-4 py-2`}
|
||||||
>
|
>
|
||||||
<option value="">Select a category</option>
|
<option value="">{formCopy.categoryPlaceholder}</option>
|
||||||
{categories?.map((category) => (
|
{categories?.map((category) => (
|
||||||
<option key={category.id} value={category.id}>
|
<option key={category.id} value={category.id}>
|
||||||
{category.name}
|
{category.name}
|
||||||
@@ -106,13 +110,13 @@ export default function UpdateItemForm({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="stock" className="mb-2 block text-lg">
|
<label htmlFor="stock" className="mb-2 block text-lg">
|
||||||
Stock
|
{formCopy.stockLabel}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="stock"
|
id="stock"
|
||||||
pattern="{[0-9]*}"
|
pattern="{[0-9]*}"
|
||||||
placeholder="0"
|
placeholder={formCopy.stockPlaceholder}
|
||||||
min={item.stock}
|
min={item.stock}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
{...register("stock")}
|
{...register("stock")}
|
||||||
@@ -139,7 +143,7 @@ export default function UpdateItemForm({
|
|||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
isSubmitSuccessful={isSubmitSuccessful}
|
isSubmitSuccessful={isSubmitSuccessful}
|
||||||
>
|
>
|
||||||
Update Item
|
{formCopy.updateSubmit}
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,14 +6,16 @@ import NewItemForm from "../_components/new.item.form"
|
|||||||
export default async function NewItemPage() {
|
export default async function NewItemPage() {
|
||||||
const categories = await CategoryService.findAll()
|
const categories = await CategoryService.findAll()
|
||||||
const { dictionary } = await getI18n()
|
const { dictionary } = await getI18n()
|
||||||
|
const copy = dictionary.inventory.items
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<h1 className="text-2xl font-bold">New Item</h1>
|
<h1 className="text-2xl font-bold">{copy.new.title}</h1>
|
||||||
</div>
|
</div>
|
||||||
<NewItemForm
|
<NewItemForm
|
||||||
categories={categories}
|
categories={categories}
|
||||||
|
formCopy={copy.form}
|
||||||
submitButtonCopy={dictionary.common.submitButton}
|
submitButtonCopy={dictionary.common.submitButton}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { ItemService } from "@/services/item.service"
|
import { ItemService } from "@/services/item.service"
|
||||||
|
|
||||||
import DeleteItemButton from "./_components/delete.item.button"
|
import DeleteItemButton from "./_components/delete.item.button"
|
||||||
@@ -22,19 +23,22 @@ export default async function ItemsPage(props: {
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
search,
|
search,
|
||||||
})
|
})
|
||||||
|
const { dictionary } = await getI18n()
|
||||||
|
const copy = dictionary.inventory.items
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Items"
|
title={copy.list.title}
|
||||||
link="/inventory/items/new"
|
link="/inventory/items/new"
|
||||||
|
addLabel={copy.list.addLabel}
|
||||||
data={items}
|
data={items}
|
||||||
search={search}
|
search={search}
|
||||||
/>
|
/>
|
||||||
{items.length === 0 && currentPage === 1 && (
|
{items.length === 0 && currentPage === 1 && (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
No items found.
|
{copy.list.empty}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -44,19 +48,19 @@ export default async function ItemsPage(props: {
|
|||||||
<thead className="border-b">
|
<thead className="border-b">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" className="p-4">
|
<th scope="col" className="p-4">
|
||||||
Name
|
{copy.list.columns.name}
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" className="p-4">
|
<th scope="col" className="p-4">
|
||||||
Category
|
{copy.list.columns.category}
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" className="p-4">
|
<th scope="col" className="p-4">
|
||||||
Assets
|
{copy.list.columns.assets}
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" className="p-4">
|
<th scope="col" className="p-4">
|
||||||
Stock
|
{copy.list.columns.stock}
|
||||||
</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>
|
||||||
@@ -69,17 +73,25 @@ export default async function ItemsPage(props: {
|
|||||||
<td className="p-4">{item.stock}</td>
|
<td className="p-4">{item.stock}</td>
|
||||||
<td className="flex items-center gap-2 p-4">
|
<td className="flex items-center gap-2 p-4">
|
||||||
<Link href={`/inventory/items/${item.id}`} passHref>
|
<Link href={`/inventory/items/${item.id}`} passHref>
|
||||||
<Button variant="outline" size="icon">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
aria-label={copy.list.actions.view}
|
||||||
|
>
|
||||||
<Eye />
|
<Eye />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={`/inventory/items/${item.id}/edit`} passHref>
|
<Link href={`/inventory/items/${item.id}/edit`} passHref>
|
||||||
<Button variant="outline" size="icon">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
aria-label={copy.list.actions.edit}
|
||||||
|
>
|
||||||
<Pencil />
|
<Pencil />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{item._count.assets === 0 && item.stock === 0 && (
|
{item._count.assets === 0 && item.stock === 0 && (
|
||||||
<DeleteItemButton itemId={item.id} />
|
<DeleteItemButton itemId={item.id} copy={copy.delete} />
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -113,6 +113,75 @@ export const en = {
|
|||||||
idRequired: "ID is required",
|
idRequired: "ID is required",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
items: {
|
||||||
|
list: {
|
||||||
|
title: "Items",
|
||||||
|
addLabel: "Add Item",
|
||||||
|
empty: "No items found.",
|
||||||
|
columns: {
|
||||||
|
name: "Name",
|
||||||
|
category: "Category",
|
||||||
|
assets: "Assets",
|
||||||
|
stock: "Stock",
|
||||||
|
actions: "Actions",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
view: "View item",
|
||||||
|
edit: "Edit item",
|
||||||
|
delete: "Delete item",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
detail: {
|
||||||
|
notFound: "Item not found",
|
||||||
|
labels: {
|
||||||
|
category: "Category",
|
||||||
|
stock: "Stock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new: {
|
||||||
|
title: "New Item",
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
title: "Edit Item",
|
||||||
|
notFound: "Item not found",
|
||||||
|
hasAssetsWarning: "This item has already assets assigned to it.",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
nameLabel: "Name",
|
||||||
|
namePlaceholder: "Item name",
|
||||||
|
categoryLabel: "Category",
|
||||||
|
categoryPlaceholder: "Select a category",
|
||||||
|
stockLabel: "Stock",
|
||||||
|
stockPlaceholder: "0",
|
||||||
|
createSubmit: "Create Item",
|
||||||
|
updateSubmit: "Update Item",
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
label: "Delete item",
|
||||||
|
pending: "Deleting...",
|
||||||
|
unknownError: "Unknown error",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
createSuccess: "Item created successfully!",
|
||||||
|
createFailure: "Error creating item",
|
||||||
|
updateSuccess: "Item updated successfully!",
|
||||||
|
updateFailure: "Failed to update item",
|
||||||
|
deleteSuccess: "Item deleted successfully!",
|
||||||
|
deleteFailure: "Failed to delete item",
|
||||||
|
duplicateName: "Item already exists",
|
||||||
|
notFound: "Item not found",
|
||||||
|
hasAssets: "Cannot delete item with assets",
|
||||||
|
hasStock: "Cannot delete item with stock",
|
||||||
|
invalidStock: "Invalid stock",
|
||||||
|
negativeStock: "Stock cannot be negative",
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
nameRequired: "Name is required",
|
||||||
|
categoryRequired: "Category is required",
|
||||||
|
stockRequired: "Stock is required",
|
||||||
|
itemRequired: "Item is required",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
title: "Sign In",
|
title: "Sign In",
|
||||||
|
|||||||
@@ -116,6 +116,75 @@ export const es = {
|
|||||||
idRequired: "El ID es obligatorio",
|
idRequired: "El ID es obligatorio",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
items: {
|
||||||
|
list: {
|
||||||
|
title: "Artículos",
|
||||||
|
addLabel: "Agregar artículo",
|
||||||
|
empty: "No se encontraron artículos.",
|
||||||
|
columns: {
|
||||||
|
name: "Nombre",
|
||||||
|
category: "Categoría",
|
||||||
|
assets: "Activos",
|
||||||
|
stock: "Stock",
|
||||||
|
actions: "Acciones",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
view: "Ver artículo",
|
||||||
|
edit: "Editar artículo",
|
||||||
|
delete: "Eliminar artículo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
detail: {
|
||||||
|
notFound: "Artículo no encontrado",
|
||||||
|
labels: {
|
||||||
|
category: "Categoría",
|
||||||
|
stock: "Stock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new: {
|
||||||
|
title: "Nuevo artículo",
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
title: "Editar artículo",
|
||||||
|
notFound: "Artículo no encontrado",
|
||||||
|
hasAssetsWarning: "Este artículo ya tiene activos asignados.",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
nameLabel: "Nombre",
|
||||||
|
namePlaceholder: "Nombre del artículo",
|
||||||
|
categoryLabel: "Categoría",
|
||||||
|
categoryPlaceholder: "Selecciona una categoría",
|
||||||
|
stockLabel: "Stock",
|
||||||
|
stockPlaceholder: "0",
|
||||||
|
createSubmit: "Crear artículo",
|
||||||
|
updateSubmit: "Actualizar artículo",
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
label: "Eliminar artículo",
|
||||||
|
pending: "Eliminando...",
|
||||||
|
unknownError: "Error desconocido",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
createSuccess: "Artículo creado correctamente",
|
||||||
|
createFailure: "Error al crear el artículo",
|
||||||
|
updateSuccess: "Artículo actualizado correctamente",
|
||||||
|
updateFailure: "Error al actualizar el artículo",
|
||||||
|
deleteSuccess: "Artículo eliminado correctamente",
|
||||||
|
deleteFailure: "Error al eliminar el artículo",
|
||||||
|
duplicateName: "El artículo ya existe",
|
||||||
|
notFound: "Artículo no encontrado",
|
||||||
|
hasAssets: "No se puede eliminar un artículo con activos",
|
||||||
|
hasStock: "No se puede eliminar un artículo con stock",
|
||||||
|
invalidStock: "Stock inválido",
|
||||||
|
negativeStock: "El stock no puede ser negativo",
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
nameRequired: "El nombre es obligatorio",
|
||||||
|
categoryRequired: "La categoría es obligatoria",
|
||||||
|
stockRequired: "El stock es obligatorio",
|
||||||
|
itemRequired: "El artículo es obligatorio",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
title: "Iniciar sesión",
|
title: "Iniciar sesión",
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { expect, type Page, test } from "@playwright/test"
|
||||||
|
|
||||||
|
async function setLocaleCookie(
|
||||||
|
page: Page,
|
||||||
|
locale: "en" | "es",
|
||||||
|
baseURL?: string,
|
||||||
|
) {
|
||||||
|
await page.context().addCookies([
|
||||||
|
{
|
||||||
|
name: "stock-manager-locale",
|
||||||
|
value: locale,
|
||||||
|
url: baseURL ?? "http://127.0.0.1:3100",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signInAsAdmin(page: Page, baseURL?: string) {
|
||||||
|
await setLocaleCookie(page, "en", baseURL)
|
||||||
|
await page.goto("/login")
|
||||||
|
await page.getByLabel("Username").fill("admin")
|
||||||
|
await page.getByLabel("Password").fill("admin-password")
|
||||||
|
await page.getByRole("button", { name: "Sign In" }).click()
|
||||||
|
await expect(page).toHaveURL("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("inventory items localization", () => {
|
||||||
|
test("renders item list and new form UI copy in Spanish", async ({
|
||||||
|
baseURL,
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await signInAsAdmin(page, baseURL)
|
||||||
|
await setLocaleCookie(page, "es", baseURL)
|
||||||
|
|
||||||
|
await page.goto("/inventory/items")
|
||||||
|
|
||||||
|
await expect(page.locator("html")).toHaveAttribute("lang", "es")
|
||||||
|
await expect(page.getByRole("heading", { name: "Artículos" })).toBeVisible()
|
||||||
|
await expect(
|
||||||
|
page.getByRole("link", { name: /Agregar artículo/ }),
|
||||||
|
).toBeVisible()
|
||||||
|
await expect(page.getByText("No se encontraron artículos.")).toBeVisible()
|
||||||
|
|
||||||
|
await page.goto("/inventory/items/new")
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: "Nuevo artículo" }),
|
||||||
|
).toBeVisible()
|
||||||
|
await expect(page.getByLabel("Nombre")).toBeVisible()
|
||||||
|
await expect(page.getByPlaceholder("Nombre del artículo")).toBeVisible()
|
||||||
|
await expect(page.getByLabel("Categoría")).toBeVisible()
|
||||||
|
await expect(page.locator("select#categoryId")).toContainText(
|
||||||
|
"Selecciona una categoría",
|
||||||
|
)
|
||||||
|
await expect(page.getByLabel("Stock")).toBeVisible()
|
||||||
|
await expect(
|
||||||
|
page.getByRole("button", { name: "Crear artículo" }),
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -266,6 +266,148 @@ describe("i18n dictionaries", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("provides localized inventory item copy for English and Spanish", () => {
|
||||||
|
expect(getDictionary("en").inventory.items).toEqual({
|
||||||
|
list: {
|
||||||
|
title: "Items",
|
||||||
|
addLabel: "Add Item",
|
||||||
|
empty: "No items found.",
|
||||||
|
columns: {
|
||||||
|
name: "Name",
|
||||||
|
category: "Category",
|
||||||
|
assets: "Assets",
|
||||||
|
stock: "Stock",
|
||||||
|
actions: "Actions",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
view: "View item",
|
||||||
|
edit: "Edit item",
|
||||||
|
delete: "Delete item",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
detail: {
|
||||||
|
notFound: "Item not found",
|
||||||
|
labels: {
|
||||||
|
category: "Category",
|
||||||
|
stock: "Stock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new: {
|
||||||
|
title: "New Item",
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
title: "Edit Item",
|
||||||
|
notFound: "Item not found",
|
||||||
|
hasAssetsWarning: "This item has already assets assigned to it.",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
nameLabel: "Name",
|
||||||
|
namePlaceholder: "Item name",
|
||||||
|
categoryLabel: "Category",
|
||||||
|
categoryPlaceholder: "Select a category",
|
||||||
|
stockLabel: "Stock",
|
||||||
|
stockPlaceholder: "0",
|
||||||
|
createSubmit: "Create Item",
|
||||||
|
updateSubmit: "Update Item",
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
label: "Delete item",
|
||||||
|
pending: "Deleting...",
|
||||||
|
unknownError: "Unknown error",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
createSuccess: "Item created successfully!",
|
||||||
|
createFailure: "Error creating item",
|
||||||
|
updateSuccess: "Item updated successfully!",
|
||||||
|
updateFailure: "Failed to update item",
|
||||||
|
deleteSuccess: "Item deleted successfully!",
|
||||||
|
deleteFailure: "Failed to delete item",
|
||||||
|
duplicateName: "Item already exists",
|
||||||
|
notFound: "Item not found",
|
||||||
|
hasAssets: "Cannot delete item with assets",
|
||||||
|
hasStock: "Cannot delete item with stock",
|
||||||
|
invalidStock: "Invalid stock",
|
||||||
|
negativeStock: "Stock cannot be negative",
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
nameRequired: "Name is required",
|
||||||
|
categoryRequired: "Category is required",
|
||||||
|
stockRequired: "Stock is required",
|
||||||
|
itemRequired: "Item is required",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getDictionary("es").inventory.items).toEqual({
|
||||||
|
list: {
|
||||||
|
title: "Artículos",
|
||||||
|
addLabel: "Agregar artículo",
|
||||||
|
empty: "No se encontraron artículos.",
|
||||||
|
columns: {
|
||||||
|
name: "Nombre",
|
||||||
|
category: "Categoría",
|
||||||
|
assets: "Activos",
|
||||||
|
stock: "Stock",
|
||||||
|
actions: "Acciones",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
view: "Ver artículo",
|
||||||
|
edit: "Editar artículo",
|
||||||
|
delete: "Eliminar artículo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
detail: {
|
||||||
|
notFound: "Artículo no encontrado",
|
||||||
|
labels: {
|
||||||
|
category: "Categoría",
|
||||||
|
stock: "Stock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new: {
|
||||||
|
title: "Nuevo artículo",
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
title: "Editar artículo",
|
||||||
|
notFound: "Artículo no encontrado",
|
||||||
|
hasAssetsWarning: "Este artículo ya tiene activos asignados.",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
nameLabel: "Nombre",
|
||||||
|
namePlaceholder: "Nombre del artículo",
|
||||||
|
categoryLabel: "Categoría",
|
||||||
|
categoryPlaceholder: "Selecciona una categoría",
|
||||||
|
stockLabel: "Stock",
|
||||||
|
stockPlaceholder: "0",
|
||||||
|
createSubmit: "Crear artículo",
|
||||||
|
updateSubmit: "Actualizar artículo",
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
label: "Eliminar artículo",
|
||||||
|
pending: "Eliminando...",
|
||||||
|
unknownError: "Error desconocido",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
createSuccess: "Artículo creado correctamente",
|
||||||
|
createFailure: "Error al crear el artículo",
|
||||||
|
updateSuccess: "Artículo actualizado correctamente",
|
||||||
|
updateFailure: "Error al actualizar el artículo",
|
||||||
|
deleteSuccess: "Artículo eliminado correctamente",
|
||||||
|
deleteFailure: "Error al eliminar el artículo",
|
||||||
|
duplicateName: "El artículo ya existe",
|
||||||
|
notFound: "Artículo no encontrado",
|
||||||
|
hasAssets: "No se puede eliminar un artículo con activos",
|
||||||
|
hasStock: "No se puede eliminar un artículo con stock",
|
||||||
|
invalidStock: "Stock inválido",
|
||||||
|
negativeStock: "El stock no puede ser negativo",
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
nameRequired: "El nombre es obligatorio",
|
||||||
|
categoryRequired: "La categoría es obligatoria",
|
||||||
|
stockRequired: "El stock es obligatorio",
|
||||||
|
itemRequired: "El artículo es obligatorio",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("keeps dashboard home dictionary keys aligned across locales", () => {
|
it("keeps dashboard home dictionary keys aligned across locales", () => {
|
||||||
expect(getDictionary("en").dashboardHome).toEqual({
|
expect(getDictionary("en").dashboardHome).toEqual({
|
||||||
heading: "Dashboard",
|
heading: "Dashboard",
|
||||||
|
|||||||
Reference in New Issue
Block a user