feat(i18n): localize inventory items UI

This commit is contained in:
2026-06-13 11:12:02 +02:00
parent 9f7d1b8ef8
commit 964b1648ca
12 changed files with 414 additions and 33 deletions
@@ -13,24 +13,26 @@ export default async function AddItem({
const categories = await CategoryService.findAll()
const item = await ItemService.findByIdWithAssetCount(itemId)
const { dictionary } = await getI18n()
const copy = dictionary.inventory.items
if (!item) {
return <div>Item not found</div>
return <div>{copy.edit.notFound}</div>
}
return (
<div className="flex flex-col gap-4">
{item?._count?.assets && item?._count.assets > 0 && (
<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 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>
<UpdateItemForm
categories={categories}
item={item}
formCopy={copy.form}
submitButtonCopy={dictionary.common.submitButton}
/>
</div>
@@ -1,4 +1,5 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { getI18n } from "@/i18n/server"
import { AssetService } from "@/services/asset.service"
import { ItemService } from "@/services/item.service"
import { MovementService } from "@/services/movement.service"
@@ -12,9 +13,11 @@ export default async function ItemPage({
const item = await ItemService.findByIdWithCategory(itemId)
const assets = await AssetService.findByItemId(itemId)
const movements = await MovementService.findAllByItemId(itemId)
const { dictionary } = await getI18n()
const copy = dictionary.inventory.items.detail
if (!item) {
return <div>Item not found</div>
return <div>{copy.notFound}</div>
}
return (
@@ -26,11 +29,11 @@ export default async function ItemPage({
<CardContent>
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Category</span>
<span className="text-gray-600">{copy.labels.category}</span>
<span>{item.category.name}</span>
</div>
<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>
</div>
</div>
@@ -7,7 +7,15 @@ import { toast } from "sonner"
import { deleteItemAction } from "@/actions/item.actions"
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 [isPending, startTransition] = useTransition()
@@ -24,7 +32,7 @@ export default function DeleteItemButton({ itemId }: { itemId: string }) {
toast.success(response.message)
router.refresh()
} 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"
variant="outline"
disabled={isPending}
aria-label={isPending ? copy.pending : copy.label}
>
<Trash />
</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"
import type { CategorySummary } from "@/types"
import type { ItemFormCopy } from "./item.copy"
export default function NewItemForm({
categories,
formCopy,
submitButtonCopy,
}: {
categories: CategorySummary[]
formCopy: ItemFormCopy
submitButtonCopy: SubmitButtonCopy
}) {
const router = useRouter()
@@ -61,12 +65,12 @@ export default function NewItemForm({
<form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="name" className="mb-2 block text-lg">
Name
{formCopy.nameLabel}
</label>
<input
type="text"
id="name"
placeholder="Item name"
placeholder={formCopy.namePlaceholder}
{...register("name")}
className="w-full rounded-lg border px-4 py-2"
/>
@@ -74,14 +78,14 @@ export default function NewItemForm({
</div>
<div>
<label htmlFor="categoryId" className="mb-2 block text-lg">
Category
{formCopy.categoryLabel}
</label>
<select
id="categoryId"
{...register("categoryId")}
className="w-full rounded-lg border px-4 py-2"
>
<option value="">Select a category</option>
<option value="">{formCopy.categoryPlaceholder}</option>
{categories?.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
@@ -94,13 +98,13 @@ export default function NewItemForm({
</div>
<div>
<label htmlFor="stock" className="mb-2 block text-lg">
Stock
{formCopy.stockLabel}
</label>
<input
type="number"
id="stock"
pattern="{[0-9]*}"
placeholder="0"
placeholder={formCopy.stockPlaceholder}
min="0"
{...register("stock")}
className="w-full rounded-lg border px-4 py-2"
@@ -124,7 +128,7 @@ export default function NewItemForm({
isSubmitting={isSubmitting}
isSubmitSuccessful={isSubmitSuccessful}
>
Create Item
{formCopy.createSubmit}
</SubmitButton>
</form>
)
@@ -15,13 +15,17 @@ import {
} from "@/schemas/item.schema"
import type { CategorySummary, ItemWithAssetCount } from "@/types"
import type { ItemFormCopy } from "./item.copy"
export default function UpdateItemForm({
categories,
item,
formCopy,
submitButtonCopy,
}: {
categories: CategorySummary[]
item: ItemWithAssetCount
formCopy: ItemFormCopy
submitButtonCopy: SubmitButtonCopy
}) {
const router = useRouter()
@@ -72,12 +76,12 @@ export default function UpdateItemForm({
{item?.id && <input type="hidden" name="id" value={item.id} />}
<div>
<label htmlFor="name" className="mb-2 block text-lg">
Name
{formCopy.nameLabel}
</label>
<input
type="text"
id="name"
placeholder="Item name"
placeholder={formCopy.namePlaceholder}
{...register("name")}
className={`w-full rounded-lg border px-4 py-2`}
/>
@@ -85,7 +89,7 @@ export default function UpdateItemForm({
</div>
<div>
<label htmlFor="categoryId" className="mb-2 block text-lg">
Category
{formCopy.categoryLabel}
</label>
<select
id="categoryId"
@@ -93,7 +97,7 @@ export default function UpdateItemForm({
{...register("categoryId")}
className={`w-full rounded-lg border px-4 py-2`}
>
<option value="">Select a category</option>
<option value="">{formCopy.categoryPlaceholder}</option>
{categories?.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
@@ -106,13 +110,13 @@ export default function UpdateItemForm({
</div>
<div>
<label htmlFor="stock" className="mb-2 block text-lg">
Stock
{formCopy.stockLabel}
</label>
<input
type="number"
id="stock"
pattern="{[0-9]*}"
placeholder="0"
placeholder={formCopy.stockPlaceholder}
min={item.stock}
disabled={isDisabled}
{...register("stock")}
@@ -139,7 +143,7 @@ export default function UpdateItemForm({
isSubmitting={isSubmitting}
isSubmitSuccessful={isSubmitSuccessful}
>
Update Item
{formCopy.updateSubmit}
</SubmitButton>
</form>
)
@@ -6,14 +6,16 @@ import NewItemForm from "../_components/new.item.form"
export default async function NewItemPage() {
const categories = await CategoryService.findAll()
const { dictionary } = await getI18n()
const copy = dictionary.inventory.items
return (
<div className="flex flex-col 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>
<NewItemForm
categories={categories}
formCopy={copy.form}
submitButtonCopy={dictionary.common.submitButton}
/>
</div>
+22 -10
View File
@@ -4,6 +4,7 @@ import Link from "next/link"
import PageHeader from "@/components/common/pageheader"
import PaginationButtons from "@/components/common/pagination"
import { Button } from "@/components/ui/button"
import { getI18n } from "@/i18n/server"
import { ItemService } from "@/services/item.service"
import DeleteItemButton from "./_components/delete.item.button"
@@ -22,19 +23,22 @@ export default async function ItemsPage(props: {
pageSize: 10,
search,
})
const { dictionary } = await getI18n()
const copy = dictionary.inventory.items
return (
<div className="flex flex-col gap-4">
<PageHeader
title="Items"
title={copy.list.title}
link="/inventory/items/new"
addLabel={copy.list.addLabel}
data={items}
search={search}
/>
{items.length === 0 && currentPage === 1 && (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-4">
No items found.
{copy.list.empty}
</div>
</div>
)}
@@ -44,19 +48,19 @@ export default async function ItemsPage(props: {
<thead className="border-b">
<tr>
<th scope="col" className="p-4">
Name
{copy.list.columns.name}
</th>
<th scope="col" className="p-4">
Category
{copy.list.columns.category}
</th>
<th scope="col" className="p-4">
Assets
{copy.list.columns.assets}
</th>
<th scope="col" className="p-4">
Stock
{copy.list.columns.stock}
</th>
<th scope="col" className="p-4">
Actions
{copy.list.columns.actions}
</th>
</tr>
</thead>
@@ -69,17 +73,25 @@ export default async function ItemsPage(props: {
<td className="p-4">{item.stock}</td>
<td className="flex items-center gap-2 p-4">
<Link href={`/inventory/items/${item.id}`} passHref>
<Button variant="outline" size="icon">
<Button
variant="outline"
size="icon"
aria-label={copy.list.actions.view}
>
<Eye />
</Button>
</Link>
<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 />
</Button>
</Link>
{item._count.assets === 0 && item.stock === 0 && (
<DeleteItemButton itemId={item.id} />
<DeleteItemButton itemId={item.id} copy={copy.delete} />
)}
</td>
</tr>
+69
View File
@@ -113,6 +113,75 @@ export const en = {
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: {
title: "Sign In",
+69
View File
@@ -116,6 +116,75 @@ export const es = {
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: {
title: "Iniciar sesión",