feat(i18n): localize asset validation messages
This commit is contained in:
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
import { revalidatePath } from "next/cache"
|
import { revalidatePath } from "next/cache"
|
||||||
import { flattenError } from "zod"
|
import { flattenError } from "zod"
|
||||||
|
import { localizeAssetFieldErrors } from "@/actions/asset.messages"
|
||||||
|
import { getI18n } from "@/i18n/server"
|
||||||
import {
|
import {
|
||||||
|
buildCreateAssetSchema,
|
||||||
|
buildUpdateAssetSchema,
|
||||||
type CreateAssetFormType,
|
type CreateAssetFormType,
|
||||||
createAssetSchema,
|
|
||||||
type UpdateAssetFormType,
|
type UpdateAssetFormType,
|
||||||
updateAssetSchema,
|
|
||||||
} from "@/schemas/asset.schema"
|
} from "@/schemas/asset.schema"
|
||||||
import { getAuthenticatedUserId } from "@/services/auth.service"
|
import { getAuthenticatedUserId } from "@/services/auth.service"
|
||||||
import {
|
import {
|
||||||
@@ -15,8 +17,11 @@ import {
|
|||||||
} from "@/use-cases/asset.use-cases"
|
} from "@/use-cases/asset.use-cases"
|
||||||
|
|
||||||
export async function createAssetAction(formData: CreateAssetFormType) {
|
export async function createAssetAction(formData: CreateAssetFormType) {
|
||||||
try {
|
const { dictionary } = await getI18n()
|
||||||
const validatedFields = createAssetSchema.safeParse(formData)
|
const copy = dictionary.inventory.assets
|
||||||
|
const validatedFields = buildCreateAssetSchema(copy.schema).safeParse(
|
||||||
|
formData,
|
||||||
|
)
|
||||||
|
|
||||||
if (!validatedFields.success) {
|
if (!validatedFields.success) {
|
||||||
return {
|
return {
|
||||||
@@ -24,6 +29,7 @@ export async function createAssetAction(formData: CreateAssetFormType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const userId = await getAuthenticatedUserId()
|
const userId = await getAuthenticatedUserId()
|
||||||
|
|
||||||
const result = await createAssetUseCase({
|
const result = await createAssetUseCase({
|
||||||
@@ -32,7 +38,10 @@ export async function createAssetAction(formData: CreateAssetFormType) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return result
|
return {
|
||||||
|
...result,
|
||||||
|
errors: localizeAssetFieldErrors(result.errors, copy.actions),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidatePath("/inventory/assets")
|
revalidatePath("/inventory/assets")
|
||||||
@@ -42,19 +51,23 @@ export async function createAssetAction(formData: CreateAssetFormType) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Asset created successfully",
|
message: copy.actions.createSuccess,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Database error:", error)
|
console.error("Database error:", error)
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Error creating asset",
|
message: copy.actions.createFailure,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateAssetAction(formData: UpdateAssetFormType) {
|
export async function updateAssetAction(formData: UpdateAssetFormType) {
|
||||||
const validatedFields = updateAssetSchema.safeParse(formData)
|
const { dictionary } = await getI18n()
|
||||||
|
const copy = dictionary.inventory.assets
|
||||||
|
const validatedFields = buildUpdateAssetSchema(copy.schema).safeParse(
|
||||||
|
formData,
|
||||||
|
)
|
||||||
|
|
||||||
if (!validatedFields.success) {
|
if (!validatedFields.success) {
|
||||||
return {
|
return {
|
||||||
@@ -71,7 +84,10 @@ export async function updateAssetAction(formData: UpdateAssetFormType) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return result
|
return {
|
||||||
|
...result,
|
||||||
|
errors: localizeAssetFieldErrors(result.errors, copy.actions),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidatePath("/inventory/assets")
|
revalidatePath("/inventory/assets")
|
||||||
@@ -81,13 +97,13 @@ export async function updateAssetAction(formData: UpdateAssetFormType) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Asset updated successfully",
|
message: copy.actions.updateSuccess,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Database error:", error)
|
console.error("Database error:", error)
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Error updating asset",
|
message: copy.actions.updateFailure,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { Dictionary } from "@/i18n/dictionaries"
|
||||||
|
|
||||||
|
type AssetActionCopy = Dictionary["inventory"]["assets"]["actions"]
|
||||||
|
|
||||||
|
type FieldErrors = Record<string, string[]>
|
||||||
|
|
||||||
|
const assetErrorMessageKeys = {
|
||||||
|
"Item not found": "itemNotFound",
|
||||||
|
"Asset not found": "notFound",
|
||||||
|
"This serial number already exists": "duplicateSerialNumber",
|
||||||
|
"Assignment already returned": "assignmentAlreadyReturned",
|
||||||
|
"Previous item not found for available asset": "previousItemNotFound",
|
||||||
|
"Item does not have enough stock": "insufficientStock",
|
||||||
|
} as const satisfies Record<string, keyof AssetActionCopy>
|
||||||
|
|
||||||
|
function isAssetErrorMessage(
|
||||||
|
message: string,
|
||||||
|
): message is keyof typeof assetErrorMessageKeys {
|
||||||
|
return message in assetErrorMessageKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
function localizeAssetMessage(message: string, copy: AssetActionCopy): string {
|
||||||
|
if (!isAssetErrorMessage(message)) return message
|
||||||
|
|
||||||
|
return copy[assetErrorMessageKeys[message]]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function localizeAssetFieldErrors(
|
||||||
|
errors: FieldErrors | undefined,
|
||||||
|
copy: AssetActionCopy,
|
||||||
|
): FieldErrors | undefined {
|
||||||
|
if (!errors) return undefined
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(errors).map(([field, messages]) => [
|
||||||
|
field,
|
||||||
|
messages.map((message) => localizeAssetMessage(message, copy)),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ export default async function EditAssetPage({
|
|||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
asset={asset as unknown as AssetWithAssignment}
|
asset={asset as unknown as AssetWithAssignment}
|
||||||
formCopy={copy.form}
|
formCopy={copy.form}
|
||||||
|
schemaCopy={copy.schema}
|
||||||
statusCopy={copy.status}
|
statusCopy={copy.status}
|
||||||
submitButtonCopy={dictionary.common.submitButton}
|
submitButtonCopy={dictionary.common.submitButton}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { Dictionary } from "@/i18n/dictionaries"
|
import type { Dictionary } from "@/i18n/dictionaries"
|
||||||
|
import type { AssetSchemaCopy } from "@/schemas/asset.schema"
|
||||||
|
|
||||||
export type AssetListCopy = Dictionary["inventory"]["assets"]["list"]
|
export type AssetListCopy = Dictionary["inventory"]["assets"]["list"]
|
||||||
export type AssetFormCopy = Dictionary["inventory"]["assets"]["form"]
|
export type AssetFormCopy = Dictionary["inventory"]["assets"]["form"]
|
||||||
export type AssetStatusCopy = Dictionary["inventory"]["assets"]["status"]
|
export type AssetStatusCopy = Dictionary["inventory"]["assets"]["status"]
|
||||||
export type AssetFallbackCopy = Dictionary["inventory"]["assets"]["fallback"]
|
export type AssetFallbackCopy = Dictionary["inventory"]["assets"]["fallback"]
|
||||||
|
export type { AssetSchemaCopy }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
|
import { useMemo } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { updateAssetAction } from "@/actions/asset.actions"
|
import { updateAssetAction } from "@/actions/asset.actions"
|
||||||
@@ -11,8 +12,8 @@ import {
|
|||||||
} from "@/components/forms/submitButton"
|
} from "@/components/forms/submitButton"
|
||||||
import { ITEM_STATUS } from "@/lib/constants"
|
import { ITEM_STATUS } from "@/lib/constants"
|
||||||
import {
|
import {
|
||||||
|
buildUpdateAssetSchema,
|
||||||
type UpdateAssetFormType,
|
type UpdateAssetFormType,
|
||||||
updateAssetSchema,
|
|
||||||
} from "@/schemas/asset.schema"
|
} from "@/schemas/asset.schema"
|
||||||
import type {
|
import type {
|
||||||
AssetWithAssignment,
|
AssetWithAssignment,
|
||||||
@@ -21,13 +22,18 @@ import type {
|
|||||||
UpdateAssetStatus,
|
UpdateAssetStatus,
|
||||||
} from "@/types"
|
} from "@/types"
|
||||||
|
|
||||||
import type { AssetFormCopy, AssetStatusCopy } from "./asset.copy"
|
import type {
|
||||||
|
AssetFormCopy,
|
||||||
|
AssetSchemaCopy,
|
||||||
|
AssetStatusCopy,
|
||||||
|
} from "./asset.copy"
|
||||||
|
|
||||||
interface EditAssetFormProps {
|
interface EditAssetFormProps {
|
||||||
asset: AssetWithAssignment
|
asset: AssetWithAssignment
|
||||||
items: Item[]
|
items: Item[]
|
||||||
recipients: Recipient[]
|
recipients: Recipient[]
|
||||||
formCopy: AssetFormCopy
|
formCopy: AssetFormCopy
|
||||||
|
schemaCopy: AssetSchemaCopy
|
||||||
statusCopy: AssetStatusCopy
|
statusCopy: AssetStatusCopy
|
||||||
submitButtonCopy: SubmitButtonCopy
|
submitButtonCopy: SubmitButtonCopy
|
||||||
}
|
}
|
||||||
@@ -37,10 +43,12 @@ export default function EditAssetForm({
|
|||||||
items,
|
items,
|
||||||
recipients,
|
recipients,
|
||||||
formCopy,
|
formCopy,
|
||||||
|
schemaCopy,
|
||||||
statusCopy,
|
statusCopy,
|
||||||
submitButtonCopy,
|
submitButtonCopy,
|
||||||
}: EditAssetFormProps) {
|
}: EditAssetFormProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const schema = useMemo(() => buildUpdateAssetSchema(schemaCopy), [schemaCopy])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -49,7 +57,7 @@ export default function EditAssetForm({
|
|||||||
formState: { errors, isSubmitting, isSubmitSuccessful },
|
formState: { errors, isSubmitting, isSubmitSuccessful },
|
||||||
watch,
|
watch,
|
||||||
} = useForm<UpdateAssetFormType>({
|
} = useForm<UpdateAssetFormType>({
|
||||||
resolver: zodResolver(updateAssetSchema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
itemId: asset.itemId ?? undefined,
|
itemId: asset.itemId ?? undefined,
|
||||||
@@ -81,7 +89,7 @@ export default function EditAssetForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response?.success) {
|
if (response?.success) {
|
||||||
toast.success("Asset updated successfully")
|
toast.success(response.message)
|
||||||
router.push(`/inventory/assets`)
|
router.push(`/inventory/assets`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
|
import { useMemo } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { createAssetAction } from "@/actions/asset.actions"
|
import { createAssetAction } from "@/actions/asset.actions"
|
||||||
@@ -11,17 +12,22 @@ import {
|
|||||||
} from "@/components/forms/submitButton"
|
} from "@/components/forms/submitButton"
|
||||||
import { ITEM_STATUS } from "@/lib/constants"
|
import { ITEM_STATUS } from "@/lib/constants"
|
||||||
import {
|
import {
|
||||||
|
buildCreateAssetSchema,
|
||||||
type CreateAssetFormType,
|
type CreateAssetFormType,
|
||||||
createAssetSchema,
|
|
||||||
} from "@/schemas/asset.schema"
|
} from "@/schemas/asset.schema"
|
||||||
import type { ItemWithoutStock, Recipient } from "@/types"
|
import type { ItemWithoutStock, Recipient } from "@/types"
|
||||||
|
|
||||||
import type { AssetFormCopy, AssetStatusCopy } from "./asset.copy"
|
import type {
|
||||||
|
AssetFormCopy,
|
||||||
|
AssetSchemaCopy,
|
||||||
|
AssetStatusCopy,
|
||||||
|
} from "./asset.copy"
|
||||||
|
|
||||||
interface NewAssetFormProps {
|
interface NewAssetFormProps {
|
||||||
items: ItemWithoutStock[]
|
items: ItemWithoutStock[]
|
||||||
recipients: Recipient[]
|
recipients: Recipient[]
|
||||||
formCopy: AssetFormCopy
|
formCopy: AssetFormCopy
|
||||||
|
schemaCopy: AssetSchemaCopy
|
||||||
statusCopy: AssetStatusCopy
|
statusCopy: AssetStatusCopy
|
||||||
submitButtonCopy: SubmitButtonCopy
|
submitButtonCopy: SubmitButtonCopy
|
||||||
}
|
}
|
||||||
@@ -30,10 +36,12 @@ export default function NewAssetForm({
|
|||||||
items,
|
items,
|
||||||
recipients,
|
recipients,
|
||||||
formCopy,
|
formCopy,
|
||||||
|
schemaCopy,
|
||||||
statusCopy,
|
statusCopy,
|
||||||
submitButtonCopy,
|
submitButtonCopy,
|
||||||
}: NewAssetFormProps) {
|
}: NewAssetFormProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const schema = useMemo(() => buildCreateAssetSchema(schemaCopy), [schemaCopy])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -42,7 +50,7 @@ export default function NewAssetForm({
|
|||||||
formState: { errors, isSubmitting, isSubmitSuccessful },
|
formState: { errors, isSubmitting, isSubmitSuccessful },
|
||||||
watch,
|
watch,
|
||||||
} = useForm<CreateAssetFormType>({
|
} = useForm<CreateAssetFormType>({
|
||||||
resolver: zodResolver(createAssetSchema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
status: "AVAILABLE",
|
status: "AVAILABLE",
|
||||||
},
|
},
|
||||||
@@ -69,7 +77,7 @@ export default function NewAssetForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response?.success) {
|
if (response?.success) {
|
||||||
toast.success("Asset created successfully")
|
toast.success(response.message)
|
||||||
router.push(`/inventory/assets`)
|
router.push(`/inventory/assets`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export default async function NewAssetPage() {
|
|||||||
items={items}
|
items={items}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
formCopy={copy.form}
|
formCopy={copy.form}
|
||||||
|
schemaCopy={copy.schema}
|
||||||
statusCopy={copy.status}
|
statusCopy={copy.status}
|
||||||
submitButtonCopy={dictionary.common.submitButton}
|
submitButtonCopy={dictionary.common.submitButton}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -231,6 +231,29 @@ export const en = {
|
|||||||
fallback: {
|
fallback: {
|
||||||
unknownStatus: "Unknown status",
|
unknownStatus: "Unknown status",
|
||||||
},
|
},
|
||||||
|
actions: {
|
||||||
|
createSuccess: "Asset created successfully",
|
||||||
|
createFailure: "Error creating asset",
|
||||||
|
updateSuccess: "Asset updated successfully",
|
||||||
|
updateFailure: "Error updating asset",
|
||||||
|
duplicateSerialNumber: "This serial number already exists",
|
||||||
|
notFound: "Asset not found",
|
||||||
|
itemNotFound: "Item not found",
|
||||||
|
assignmentAlreadyReturned: "Assignment already returned",
|
||||||
|
previousItemNotFound: "Previous item not found for available asset",
|
||||||
|
insufficientStock: "Item does not have enough stock",
|
||||||
|
recipientRequired: "Recipient is required",
|
||||||
|
invalidStatus: "Invalid status",
|
||||||
|
genericFailure: "Error processing asset",
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
itemRequired: "Item is required",
|
||||||
|
serialNumberRequired: "Serial number is required",
|
||||||
|
idRequired: "ID is required",
|
||||||
|
statusRequired: "Status is required",
|
||||||
|
invalidCreateStatus: "Status must be Available or Assigned",
|
||||||
|
invalidUpdateStatus: "Invalid status",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
|
|||||||
@@ -234,6 +234,30 @@ export const es = {
|
|||||||
fallback: {
|
fallback: {
|
||||||
unknownStatus: "Estado desconocido",
|
unknownStatus: "Estado desconocido",
|
||||||
},
|
},
|
||||||
|
actions: {
|
||||||
|
createSuccess: "Activo creado correctamente",
|
||||||
|
createFailure: "Error al crear el activo",
|
||||||
|
updateSuccess: "Activo actualizado correctamente",
|
||||||
|
updateFailure: "Error al actualizar el activo",
|
||||||
|
duplicateSerialNumber: "El número de serie ya existe",
|
||||||
|
notFound: "Activo no encontrado",
|
||||||
|
itemNotFound: "Artículo no encontrado",
|
||||||
|
assignmentAlreadyReturned: "La asignación ya fue devuelta",
|
||||||
|
previousItemNotFound:
|
||||||
|
"Artículo anterior no encontrado para el activo disponible",
|
||||||
|
insufficientStock: "El artículo no tiene stock suficiente",
|
||||||
|
recipientRequired: "El destinatario es obligatorio",
|
||||||
|
invalidStatus: "Estado inválido",
|
||||||
|
genericFailure: "Error al procesar el activo",
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
itemRequired: "El artículo es obligatorio",
|
||||||
|
serialNumberRequired: "El número de serie es obligatorio",
|
||||||
|
idRequired: "El activo es obligatorio",
|
||||||
|
statusRequired: "El estado es obligatorio",
|
||||||
|
invalidCreateStatus: "El estado inicial debe ser Disponible o Asignado",
|
||||||
|
invalidUpdateStatus: "Estado inválido",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
|
|||||||
+61
-23
@@ -1,29 +1,20 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
export const assetSchema = z.object({
|
import type { Dictionary } from "@/i18n/dictionaries"
|
||||||
id: z.string().optional(),
|
|
||||||
itemId: z.string().min(1, {
|
|
||||||
error: "Item is required",
|
|
||||||
}),
|
|
||||||
serialNumber: z.string().min(1, {
|
|
||||||
error: "Serial number is required",
|
|
||||||
}),
|
|
||||||
deliveryNote: z.string().optional(),
|
|
||||||
notes: z.string().optional(),
|
|
||||||
recipientId: z.string().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const createAssetSchema = assetSchema.extend({
|
export type AssetSchemaCopy = Dictionary["inventory"]["assets"]["schema"]
|
||||||
status: z.enum(["AVAILABLE", "ASSIGNED"]),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type CreateAssetFormType = z.infer<typeof createAssetSchema>
|
const defaultAssetSchemaCopy: AssetSchemaCopy = {
|
||||||
|
itemRequired: "Item is required",
|
||||||
|
serialNumberRequired: "Serial number is required",
|
||||||
|
idRequired: "ID is required",
|
||||||
|
statusRequired: "Status is required",
|
||||||
|
invalidCreateStatus: "Status must be Available or Assigned",
|
||||||
|
invalidUpdateStatus: "Invalid status",
|
||||||
|
}
|
||||||
|
|
||||||
export const updateAssetSchema = assetSchema.extend({
|
const createAssetStatuses = ["AVAILABLE", "ASSIGNED"] as const
|
||||||
id: z.string().min(1, {
|
const updateAssetStatuses = [
|
||||||
error: "ID is required",
|
|
||||||
}),
|
|
||||||
status: z.enum([
|
|
||||||
"AVAILABLE",
|
"AVAILABLE",
|
||||||
"ASSIGNED",
|
"ASSIGNED",
|
||||||
"RESERVED",
|
"RESERVED",
|
||||||
@@ -31,7 +22,54 @@ export const updateAssetSchema = assetSchema.extend({
|
|||||||
"BROKEN",
|
"BROKEN",
|
||||||
"STOLEN",
|
"STOLEN",
|
||||||
"DISPOSED",
|
"DISPOSED",
|
||||||
]),
|
] as const
|
||||||
})
|
|
||||||
|
function buildAssetBaseSchema(copy: AssetSchemaCopy) {
|
||||||
|
return z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
itemId: z.string().min(1, {
|
||||||
|
error: copy.itemRequired,
|
||||||
|
}),
|
||||||
|
serialNumber: z.string().min(1, {
|
||||||
|
error: copy.serialNumberRequired,
|
||||||
|
}),
|
||||||
|
deliveryNote: z.string().optional(),
|
||||||
|
notes: z.string().optional(),
|
||||||
|
recipientId: z.string().optional(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const assetSchema = buildAssetBaseSchema(defaultAssetSchemaCopy)
|
||||||
|
|
||||||
|
export function buildCreateAssetSchema(copy: AssetSchemaCopy) {
|
||||||
|
return buildAssetBaseSchema(copy).extend({
|
||||||
|
status: z.enum(createAssetStatuses, {
|
||||||
|
error: (issue) =>
|
||||||
|
issue.input === undefined || issue.input === ""
|
||||||
|
? copy.statusRequired
|
||||||
|
: copy.invalidCreateStatus,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createAssetSchema = buildCreateAssetSchema(defaultAssetSchemaCopy)
|
||||||
|
|
||||||
|
export type CreateAssetFormType = z.infer<typeof createAssetSchema>
|
||||||
|
|
||||||
|
export function buildUpdateAssetSchema(copy: AssetSchemaCopy) {
|
||||||
|
return buildAssetBaseSchema(copy).extend({
|
||||||
|
id: z.string().min(1, {
|
||||||
|
error: copy.idRequired,
|
||||||
|
}),
|
||||||
|
status: z.enum(updateAssetStatuses, {
|
||||||
|
error: (issue) =>
|
||||||
|
issue.input === undefined || issue.input === ""
|
||||||
|
? copy.statusRequired
|
||||||
|
: copy.invalidUpdateStatus,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateAssetSchema = buildUpdateAssetSchema(defaultAssetSchemaCopy)
|
||||||
|
|
||||||
export type UpdateAssetFormType = z.infer<typeof updateAssetSchema>
|
export type UpdateAssetFormType = z.infer<typeof updateAssetSchema>
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import { localizeAssetFieldErrors } from "@/actions/asset.messages"
|
||||||
|
|
||||||
|
const actionCopy = {
|
||||||
|
createSuccess: "Activo creado correctamente",
|
||||||
|
createFailure: "Error al crear el activo",
|
||||||
|
updateSuccess: "Activo actualizado correctamente",
|
||||||
|
updateFailure: "Error al actualizar el activo",
|
||||||
|
duplicateSerialNumber: "El número de serie ya existe",
|
||||||
|
notFound: "Activo no encontrado",
|
||||||
|
itemNotFound: "Artículo no encontrado",
|
||||||
|
assignmentAlreadyReturned: "La asignación ya fue devuelta",
|
||||||
|
previousItemNotFound: "Artículo anterior no encontrado",
|
||||||
|
insufficientStock: "El artículo no tiene stock suficiente",
|
||||||
|
recipientRequired: "El destinatario es obligatorio",
|
||||||
|
invalidStatus: "Estado inválido",
|
||||||
|
genericFailure: "Error al procesar el activo",
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("asset action message localization", () => {
|
||||||
|
it("localizes known asset field errors", () => {
|
||||||
|
expect(
|
||||||
|
localizeAssetFieldErrors(
|
||||||
|
{
|
||||||
|
itemId: ["Item not found"],
|
||||||
|
id: ["Asset not found", "Assignment already returned"],
|
||||||
|
serialNumber: ["This serial number already exists"],
|
||||||
|
stock: ["Item does not have enough stock"],
|
||||||
|
previousItem: ["Previous item not found for available asset"],
|
||||||
|
},
|
||||||
|
actionCopy,
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
itemId: [actionCopy.itemNotFound],
|
||||||
|
id: [actionCopy.notFound, actionCopy.assignmentAlreadyReturned],
|
||||||
|
serialNumber: [actionCopy.duplicateSerialNumber],
|
||||||
|
stock: [actionCopy.insufficientStock],
|
||||||
|
previousItem: [actionCopy.previousItemNotFound],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("keeps unknown messages unchanged", () => {
|
||||||
|
expect(
|
||||||
|
localizeAssetFieldErrors(
|
||||||
|
{ serialNumber: ["Unexpected asset issue"] },
|
||||||
|
actionCopy,
|
||||||
|
),
|
||||||
|
).toEqual({ serialNumber: ["Unexpected asset issue"] })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -458,6 +458,29 @@ describe("i18n dictionaries", () => {
|
|||||||
fallback: {
|
fallback: {
|
||||||
unknownStatus: "Unknown status",
|
unknownStatus: "Unknown status",
|
||||||
},
|
},
|
||||||
|
actions: {
|
||||||
|
createSuccess: "Asset created successfully",
|
||||||
|
createFailure: "Error creating asset",
|
||||||
|
updateSuccess: "Asset updated successfully",
|
||||||
|
updateFailure: "Error updating asset",
|
||||||
|
duplicateSerialNumber: "This serial number already exists",
|
||||||
|
notFound: "Asset not found",
|
||||||
|
itemNotFound: "Item not found",
|
||||||
|
assignmentAlreadyReturned: "Assignment already returned",
|
||||||
|
previousItemNotFound: "Previous item not found for available asset",
|
||||||
|
insufficientStock: "Item does not have enough stock",
|
||||||
|
recipientRequired: "Recipient is required",
|
||||||
|
invalidStatus: "Invalid status",
|
||||||
|
genericFailure: "Error processing asset",
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
itemRequired: "Item is required",
|
||||||
|
serialNumberRequired: "Serial number is required",
|
||||||
|
idRequired: "ID is required",
|
||||||
|
statusRequired: "Status is required",
|
||||||
|
invalidCreateStatus: "Status must be Available or Assigned",
|
||||||
|
invalidUpdateStatus: "Invalid status",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getDictionary("es").inventory.assets).toEqual({
|
expect(getDictionary("es").inventory.assets).toEqual({
|
||||||
@@ -509,6 +532,30 @@ describe("i18n dictionaries", () => {
|
|||||||
fallback: {
|
fallback: {
|
||||||
unknownStatus: "Estado desconocido",
|
unknownStatus: "Estado desconocido",
|
||||||
},
|
},
|
||||||
|
actions: {
|
||||||
|
createSuccess: "Activo creado correctamente",
|
||||||
|
createFailure: "Error al crear el activo",
|
||||||
|
updateSuccess: "Activo actualizado correctamente",
|
||||||
|
updateFailure: "Error al actualizar el activo",
|
||||||
|
duplicateSerialNumber: "El número de serie ya existe",
|
||||||
|
notFound: "Activo no encontrado",
|
||||||
|
itemNotFound: "Artículo no encontrado",
|
||||||
|
assignmentAlreadyReturned: "La asignación ya fue devuelta",
|
||||||
|
previousItemNotFound:
|
||||||
|
"Artículo anterior no encontrado para el activo disponible",
|
||||||
|
insufficientStock: "El artículo no tiene stock suficiente",
|
||||||
|
recipientRequired: "El destinatario es obligatorio",
|
||||||
|
invalidStatus: "Estado inválido",
|
||||||
|
genericFailure: "Error al procesar el activo",
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
itemRequired: "El artículo es obligatorio",
|
||||||
|
serialNumberRequired: "El número de serie es obligatorio",
|
||||||
|
idRequired: "El activo es obligatorio",
|
||||||
|
statusRequired: "El estado es obligatorio",
|
||||||
|
invalidCreateStatus: "El estado inicial debe ser Disponible o Asignado",
|
||||||
|
invalidUpdateStatus: "Estado inválido",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildCreateAssetSchema,
|
||||||
|
buildUpdateAssetSchema,
|
||||||
|
} from "@/schemas/asset.schema"
|
||||||
|
|
||||||
|
const schemaCopy = {
|
||||||
|
itemRequired: "El artículo es obligatorio",
|
||||||
|
serialNumberRequired: "El número de serie es obligatorio",
|
||||||
|
idRequired: "El activo es obligatorio",
|
||||||
|
statusRequired: "El estado es obligatorio",
|
||||||
|
invalidCreateStatus: "El estado inicial no es válido",
|
||||||
|
invalidUpdateStatus: "El estado no es válido",
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("asset schema localization", () => {
|
||||||
|
it("uses localized create validation messages", () => {
|
||||||
|
const result = buildCreateAssetSchema(schemaCopy).safeParse({
|
||||||
|
itemId: "",
|
||||||
|
serialNumber: "",
|
||||||
|
status: "BROKEN",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
if (!result.success) {
|
||||||
|
const errors = result.error.flatten().fieldErrors
|
||||||
|
|
||||||
|
expect(errors.itemId).toContain(schemaCopy.itemRequired)
|
||||||
|
expect(errors.serialNumber).toContain(schemaCopy.serialNumberRequired)
|
||||||
|
expect(errors.status).toContain(schemaCopy.invalidCreateStatus)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("uses localized update identifier and status validation messages", () => {
|
||||||
|
const result = buildUpdateAssetSchema(schemaCopy).safeParse({
|
||||||
|
id: "",
|
||||||
|
itemId: "item-1",
|
||||||
|
serialNumber: "SERIAL-1",
|
||||||
|
status: "UNSUPPORTED",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
if (!result.success) {
|
||||||
|
const errors = result.error.flatten().fieldErrors
|
||||||
|
|
||||||
|
expect(errors.id).toContain(schemaCopy.idRequired)
|
||||||
|
expect(errors.status).toContain(schemaCopy.invalidUpdateStatus)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("preserves raw status values for accepted create and update payloads", () => {
|
||||||
|
const createResult = buildCreateAssetSchema(schemaCopy).safeParse({
|
||||||
|
itemId: "item-1",
|
||||||
|
serialNumber: "SERIAL-1",
|
||||||
|
status: "ASSIGNED",
|
||||||
|
recipientId: "recipient-1",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(createResult.success).toBe(true)
|
||||||
|
if (createResult.success) {
|
||||||
|
expect(createResult.data.status).toBe("ASSIGNED")
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateResult = buildUpdateAssetSchema(schemaCopy).safeParse({
|
||||||
|
id: "asset-1",
|
||||||
|
itemId: "item-1",
|
||||||
|
serialNumber: "SERIAL-1",
|
||||||
|
status: "DISPOSED",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(updateResult.success).toBe(true)
|
||||||
|
if (updateResult.success) {
|
||||||
|
expect(updateResult.data.status).toBe("DISPOSED")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("keeps optional asset fields optional", () => {
|
||||||
|
const result = buildCreateAssetSchema(schemaCopy).safeParse({
|
||||||
|
itemId: "item-1",
|
||||||
|
serialNumber: "SERIAL-1",
|
||||||
|
status: "AVAILABLE",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user