feat(i18n): localize assignment validation messages
This commit is contained in:
@@ -1,12 +1,15 @@
|
|||||||
"use server"
|
"use server"
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache"
|
import { revalidatePath } from "next/cache"
|
||||||
|
import { flattenError } from "zod"
|
||||||
|
import { localizeAssignmentFieldErrors } from "@/actions/assignment.messages"
|
||||||
|
import { getI18n } from "@/i18n/server"
|
||||||
import {
|
import {
|
||||||
assignmentSchema,
|
buildCreateAssignmentSchema,
|
||||||
|
buildUpdateAssignmentSchema,
|
||||||
type CreateAssignmentFormType,
|
type CreateAssignmentFormType,
|
||||||
type ReturnAssignmentFormType,
|
type ReturnAssignmentFormType,
|
||||||
type UpdateAssignmentFormType,
|
type UpdateAssignmentFormType,
|
||||||
updateAssignmentSchema,
|
|
||||||
} from "@/schemas/assignment.schema"
|
} from "@/schemas/assignment.schema"
|
||||||
import { getAuthenticatedUserId } from "@/services/auth.service"
|
import { getAuthenticatedUserId } from "@/services/auth.service"
|
||||||
import {
|
import {
|
||||||
@@ -16,52 +19,60 @@ import {
|
|||||||
} from "@/use-cases/assignment.use-cases"
|
} from "@/use-cases/assignment.use-cases"
|
||||||
|
|
||||||
export async function createAssignment(formData: CreateAssignmentFormType) {
|
export async function createAssignment(formData: CreateAssignmentFormType) {
|
||||||
const createdBy = await getAuthenticatedUserId()
|
const { dictionary } = await getI18n()
|
||||||
|
const copy = dictionary.inventory.assignments
|
||||||
|
|
||||||
const validatedFields = assignmentSchema.safeParse({
|
const validatedFields = buildCreateAssignmentSchema(copy.schema).safeParse(
|
||||||
...formData,
|
formData,
|
||||||
createdBy,
|
)
|
||||||
})
|
|
||||||
|
|
||||||
if (!validatedFields.success) {
|
if (!validatedFields.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
errors: flattenError(validatedFields.error).fieldErrors,
|
||||||
errors: validatedFields.error.flatten().fieldErrors,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const createdBy = await getAuthenticatedUserId()
|
||||||
|
|
||||||
const result = await createAssignmentUseCase({
|
const result = await createAssignmentUseCase({
|
||||||
...validatedFields.data,
|
...validatedFields.data,
|
||||||
actorId: createdBy,
|
actorId: createdBy,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return result
|
return {
|
||||||
|
...result,
|
||||||
|
errors: localizeAssignmentFieldErrors(result.errors, copy.actions),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidatePath("/assignments")
|
revalidatePath("/assignments")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true as const,
|
||||||
message: "Assignment 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 as const,
|
||||||
errors: { error: ["Error creating assignment"] },
|
message: copy.actions.createFailure,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateAssignment(formData: UpdateAssignmentFormType) {
|
export async function updateAssignment(formData: UpdateAssignmentFormType) {
|
||||||
const validatedFields = updateAssignmentSchema.safeParse(formData)
|
const { dictionary } = await getI18n()
|
||||||
|
const copy = dictionary.inventory.assignments
|
||||||
|
|
||||||
|
const validatedFields = buildUpdateAssignmentSchema(copy.schema).safeParse(
|
||||||
|
formData,
|
||||||
|
)
|
||||||
|
|
||||||
if (!validatedFields.success) {
|
if (!validatedFields.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
errors: flattenError(validatedFields.error).fieldErrors,
|
||||||
errors: validatedFields.error.flatten().fieldErrors,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,37 +85,42 @@ export async function updateAssignment(formData: UpdateAssignmentFormType) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return result
|
return {
|
||||||
|
...result,
|
||||||
|
errors: localizeAssignmentFieldErrors(result.errors, copy.actions),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidatePath("/assignments")
|
revalidatePath("/assignments")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true as const,
|
||||||
message: "Assignment 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 as const,
|
||||||
errors: { error: ["Error updating assignment"] },
|
message: copy.actions.updateFailure,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function returnAssignment(formData: ReturnAssignmentFormType) {
|
export async function returnAssignment(formData: ReturnAssignmentFormType) {
|
||||||
const { id } = formData
|
const { dictionary } = await getI18n()
|
||||||
|
const copy = dictionary.inventory.assignments
|
||||||
const userId = await getAuthenticatedUserId()
|
const userId = await getAuthenticatedUserId()
|
||||||
|
|
||||||
const result = await returnAssignmentUseCase({
|
const result = await returnAssignmentUseCase({
|
||||||
id,
|
id: formData.id,
|
||||||
actorId: userId,
|
actorId: userId,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
message: "Error returning assignment",
|
errors: localizeAssignmentFieldErrors(result.errors, copy.actions),
|
||||||
|
message: copy.actions.returnFailure,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +128,6 @@ export async function returnAssignment(formData: ReturnAssignmentFormType) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true as const,
|
success: true as const,
|
||||||
message: "Assignment returned successfully",
|
message: copy.actions.returnSuccess,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { Dictionary } from "@/i18n/dictionaries"
|
||||||
|
|
||||||
|
type AssignmentActionCopy = Dictionary["inventory"]["assignments"]["actions"]
|
||||||
|
|
||||||
|
type FieldErrors = Record<string, string[]>
|
||||||
|
|
||||||
|
const assignmentErrorMessageKeys = {
|
||||||
|
"Item not found": "itemNotFound",
|
||||||
|
"Item does not have enough stock": "itemInsufficientStock",
|
||||||
|
"Asset not found": "assetNotFound",
|
||||||
|
"Asset does not belong to item": "assetItemMismatch",
|
||||||
|
"Assignment not found": "notFound",
|
||||||
|
"Assignment already returned": "assignmentAlreadyReturned",
|
||||||
|
"Invalid assignment data": "invalidData",
|
||||||
|
} as const satisfies Record<string, keyof AssignmentActionCopy>
|
||||||
|
|
||||||
|
function isAssignmentErrorMessage(
|
||||||
|
message: string,
|
||||||
|
): message is keyof typeof assignmentErrorMessageKeys {
|
||||||
|
return message in assignmentErrorMessageKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
function localizeAssignmentMessage(
|
||||||
|
message: string,
|
||||||
|
copy: AssignmentActionCopy,
|
||||||
|
): string {
|
||||||
|
if (!isAssignmentErrorMessage(message)) return message
|
||||||
|
|
||||||
|
return copy[assignmentErrorMessageKeys[message]]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function localizeAssignmentFieldErrors(
|
||||||
|
errors: FieldErrors | undefined,
|
||||||
|
copy: AssignmentActionCopy,
|
||||||
|
): FieldErrors | undefined {
|
||||||
|
if (!errors) return undefined
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(errors).map(([field, messages]) => [
|
||||||
|
field,
|
||||||
|
messages.map((message) => localizeAssignmentMessage(message, copy)),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ export default async function EditAssignmentPage({
|
|||||||
assets={assets}
|
assets={assets}
|
||||||
initialData={assignment as UpdateAssignmentFormType}
|
initialData={assignment as UpdateAssignmentFormType}
|
||||||
formCopy={copy.form}
|
formCopy={copy.form}
|
||||||
|
schemaCopy={copy.schema}
|
||||||
submitButtonCopy={dictionary.common.submitButton}
|
submitButtonCopy={dictionary.common.submitButton}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 { updateAssignment } from "@/actions/assignment.actions"
|
import { updateAssignment } from "@/actions/assignment.actions"
|
||||||
@@ -11,12 +12,13 @@ import {
|
|||||||
} from "@/components/forms/submitButton"
|
} from "@/components/forms/submitButton"
|
||||||
import type { Dictionary } from "@/i18n/dictionaries"
|
import type { Dictionary } from "@/i18n/dictionaries"
|
||||||
import {
|
import {
|
||||||
|
buildUpdateAssignmentSchema,
|
||||||
type UpdateAssignmentFormType,
|
type UpdateAssignmentFormType,
|
||||||
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"]
|
type AssignmentFormCopy = Dictionary["inventory"]["assignments"]["form"]
|
||||||
|
type AssignmentSchemaCopy = Dictionary["inventory"]["assignments"]["schema"]
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
recipients: Recipient[]
|
recipients: Recipient[]
|
||||||
@@ -24,6 +26,7 @@ interface Props {
|
|||||||
assets: Asset[]
|
assets: Asset[]
|
||||||
initialData: UpdateAssignmentFormType
|
initialData: UpdateAssignmentFormType
|
||||||
formCopy: AssignmentFormCopy
|
formCopy: AssignmentFormCopy
|
||||||
|
schemaCopy: AssignmentSchemaCopy
|
||||||
submitButtonCopy: SubmitButtonCopy
|
submitButtonCopy: SubmitButtonCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,17 +36,23 @@ export default function EditAssignmentForm({
|
|||||||
assets,
|
assets,
|
||||||
initialData,
|
initialData,
|
||||||
formCopy,
|
formCopy,
|
||||||
|
schemaCopy,
|
||||||
submitButtonCopy,
|
submitButtonCopy,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
const schema = useMemo(
|
||||||
|
() => buildUpdateAssignmentSchema(schemaCopy),
|
||||||
|
[schemaCopy],
|
||||||
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isSubmitting, isSubmitSuccessful },
|
formState: { errors, isSubmitting, isSubmitSuccessful },
|
||||||
watch,
|
watch,
|
||||||
} = useForm<UpdateAssignmentFormType>({
|
} = useForm<UpdateAssignmentFormType>({
|
||||||
resolver: zodResolver(updateAssignmentSchema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...initialData,
|
...initialData,
|
||||||
id: initialData.id || undefined,
|
id: initialData.id || undefined,
|
||||||
|
|||||||
@@ -12,18 +12,20 @@ import {
|
|||||||
} from "@/components/forms/submitButton"
|
} from "@/components/forms/submitButton"
|
||||||
import type { Dictionary } from "@/i18n/dictionaries"
|
import type { Dictionary } from "@/i18n/dictionaries"
|
||||||
import {
|
import {
|
||||||
|
buildCreateAssignmentSchema,
|
||||||
type CreateAssignmentFormType,
|
type CreateAssignmentFormType,
|
||||||
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"]
|
type AssignmentFormCopy = Dictionary["inventory"]["assignments"]["form"]
|
||||||
|
type AssignmentSchemaCopy = Dictionary["inventory"]["assignments"]["schema"]
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
recipients: Recipient[]
|
recipients: Recipient[]
|
||||||
items: Item[]
|
items: Item[]
|
||||||
assets: Asset[]
|
assets: Asset[]
|
||||||
formCopy: AssignmentFormCopy
|
formCopy: AssignmentFormCopy
|
||||||
|
schemaCopy: AssignmentSchemaCopy
|
||||||
submitButtonCopy: SubmitButtonCopy
|
submitButtonCopy: SubmitButtonCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,17 +34,23 @@ export default function CreateAssignmentForm({
|
|||||||
items,
|
items,
|
||||||
assets,
|
assets,
|
||||||
formCopy,
|
formCopy,
|
||||||
|
schemaCopy,
|
||||||
submitButtonCopy,
|
submitButtonCopy,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
const schema = useMemo(
|
||||||
|
() => buildCreateAssignmentSchema(schemaCopy),
|
||||||
|
[schemaCopy],
|
||||||
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isSubmitting, isSubmitSuccessful },
|
formState: { errors, isSubmitting, isSubmitSuccessful },
|
||||||
watch,
|
watch,
|
||||||
} = useForm<CreateAssignmentFormType>({
|
} = useForm<CreateAssignmentFormType>({
|
||||||
resolver: zodResolver(createAssignmentSchema),
|
resolver: zodResolver(schema),
|
||||||
mode: "onSubmit",
|
mode: "onSubmit",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default async function NewAssignmentPage() {
|
|||||||
items={items}
|
items={items}
|
||||||
assets={assets}
|
assets={assets}
|
||||||
formCopy={copy.form}
|
formCopy={copy.form}
|
||||||
|
schemaCopy={copy.schema}
|
||||||
submitButtonCopy={dictionary.common.submitButton}
|
submitButtonCopy={dictionary.common.submitButton}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -294,6 +294,29 @@ export const en = {
|
|||||||
fallback: {
|
fallback: {
|
||||||
missingValue: "N/A",
|
missingValue: "N/A",
|
||||||
},
|
},
|
||||||
|
actions: {
|
||||||
|
createSuccess: "Assignment created successfully",
|
||||||
|
createFailure: "Error creating assignment",
|
||||||
|
updateSuccess: "Assignment updated successfully",
|
||||||
|
updateFailure: "Error updating assignment",
|
||||||
|
returnSuccess: "Assignment returned successfully",
|
||||||
|
returnFailure: "Error returning assignment",
|
||||||
|
notFound: "Assignment not found",
|
||||||
|
itemNotFound: "Item not found",
|
||||||
|
itemInsufficientStock: "Item does not have enough stock",
|
||||||
|
assetNotFound: "Asset not found",
|
||||||
|
assetItemMismatch: "Asset does not belong to item",
|
||||||
|
assignmentAlreadyReturned: "Assignment already returned",
|
||||||
|
invalidData: "Invalid assignment data",
|
||||||
|
genericFailure: "Error processing assignment",
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
recipientRequired: "Recipient is required",
|
||||||
|
itemIdRequired: "Item is required",
|
||||||
|
quantityMinOne: "Quantity must be at least 1",
|
||||||
|
assetIdRequired: "Asset ID is required when item ID is provided",
|
||||||
|
idRequired: "Assignment ID is required",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
recipients: {
|
recipients: {
|
||||||
list: {
|
list: {
|
||||||
|
|||||||
@@ -298,6 +298,30 @@ export const es = {
|
|||||||
fallback: {
|
fallback: {
|
||||||
missingValue: "No disponible",
|
missingValue: "No disponible",
|
||||||
},
|
},
|
||||||
|
actions: {
|
||||||
|
createSuccess: "Asignación creada correctamente",
|
||||||
|
createFailure: "Error al crear la asignación",
|
||||||
|
updateSuccess: "Asignación actualizada correctamente",
|
||||||
|
updateFailure: "Error al actualizar la asignación",
|
||||||
|
returnSuccess: "Asignación devuelta correctamente",
|
||||||
|
returnFailure: "Error al devolver la asignación",
|
||||||
|
notFound: "Asignación no encontrada",
|
||||||
|
itemNotFound: "Artículo no encontrado",
|
||||||
|
itemInsufficientStock: "El artículo no tiene stock suficiente",
|
||||||
|
assetNotFound: "Activo no encontrado",
|
||||||
|
assetItemMismatch: "El activo no pertenece al artículo",
|
||||||
|
assignmentAlreadyReturned: "La asignación ya fue devuelta",
|
||||||
|
invalidData: "Datos de asignación inválidos",
|
||||||
|
genericFailure: "Error al procesar la asignación",
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
recipientRequired: "El destinatario es obligatorio",
|
||||||
|
itemIdRequired: "El artículo es obligatorio",
|
||||||
|
quantityMinOne: "La cantidad debe ser al menos 1",
|
||||||
|
assetIdRequired:
|
||||||
|
"El activo es obligatorio cuando se especifica el artículo",
|
||||||
|
idRequired: "El ID de asignación es obligatorio",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
recipients: {
|
recipients: {
|
||||||
list: {
|
list: {
|
||||||
|
|||||||
@@ -1,52 +1,87 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
export const assignmentSchema = z.object({
|
import type { Dictionary } from "@/i18n/dictionaries"
|
||||||
id: z.string().optional(),
|
|
||||||
quantity: z.coerce.number().int().nonnegative().min(1, {
|
|
||||||
error: "Quantity is required",
|
|
||||||
}),
|
|
||||||
notes: z.string().optional(),
|
|
||||||
itemId: z
|
|
||||||
.string()
|
|
||||||
.min(1, {
|
|
||||||
error: "Item is required",
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
assetId: z.string().optional(),
|
|
||||||
recipientId: z.string().min(1, {
|
|
||||||
error: "Recipient is required",
|
|
||||||
}),
|
|
||||||
assignmentDate: z.date().optional(),
|
|
||||||
returnDate: z.date().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const createAssignmentSchema = assignmentSchema.omit({
|
export type AssignmentSchemaCopy =
|
||||||
id: true,
|
Dictionary["inventory"]["assignments"]["schema"]
|
||||||
returnDate: true,
|
|
||||||
})
|
const defaultAssignmentSchemaCopy: AssignmentSchemaCopy = {
|
||||||
|
recipientRequired: "Recipient is required",
|
||||||
|
itemIdRequired: "Item is required",
|
||||||
|
quantityMinOne: "Quantity must be at least 1",
|
||||||
|
assetIdRequired: "Asset ID is required when item ID is provided",
|
||||||
|
idRequired: "Assignment ID is required",
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAssignmentBaseSchema(copy: AssignmentSchemaCopy) {
|
||||||
|
return z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
quantity: z.coerce.number().int().nonnegative().min(1, {
|
||||||
|
error: copy.quantityMinOne,
|
||||||
|
}),
|
||||||
|
notes: z.string().optional(),
|
||||||
|
itemId: z
|
||||||
|
.string()
|
||||||
|
.min(1, {
|
||||||
|
error: copy.itemIdRequired,
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
assetId: z.string().optional(),
|
||||||
|
recipientId: z.string().min(1, {
|
||||||
|
error: copy.recipientRequired,
|
||||||
|
}),
|
||||||
|
assignmentDate: z.date().optional(),
|
||||||
|
returnDate: z.date().optional(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const assignmentSchema = buildAssignmentBaseSchema(
|
||||||
|
defaultAssignmentSchemaCopy,
|
||||||
|
)
|
||||||
|
|
||||||
|
export function buildCreateAssignmentSchema(copy: AssignmentSchemaCopy) {
|
||||||
|
return buildAssignmentBaseSchema(copy).omit({
|
||||||
|
id: true,
|
||||||
|
returnDate: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createAssignmentSchema = buildCreateAssignmentSchema(
|
||||||
|
defaultAssignmentSchemaCopy,
|
||||||
|
)
|
||||||
export type CreateAssignmentFormType = z.input<typeof createAssignmentSchema>
|
export type CreateAssignmentFormType = z.input<typeof createAssignmentSchema>
|
||||||
export type CreateAssignmentData = z.output<typeof createAssignmentSchema>
|
export type CreateAssignmentData = z.output<typeof createAssignmentSchema>
|
||||||
|
|
||||||
export const updateAssignmentSchema = assignmentSchema
|
export function buildUpdateAssignmentSchema(copy: AssignmentSchemaCopy) {
|
||||||
.omit({
|
return buildAssignmentBaseSchema(copy)
|
||||||
returnDate: true,
|
.omit({
|
||||||
})
|
returnDate: true,
|
||||||
.superRefine((data, ctx) => {
|
})
|
||||||
if (data.itemId && !data.assetId) {
|
.extend({
|
||||||
ctx.addIssue({
|
id: z.string().min(1, {
|
||||||
code: "custom",
|
error: copy.idRequired,
|
||||||
message: "Asset ID is required when item ID is provided",
|
}),
|
||||||
path: ["assetId"],
|
})
|
||||||
})
|
.superRefine((data, ctx) => {
|
||||||
}
|
if (data.itemId && !data.assetId) {
|
||||||
})
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: copy.assetIdRequired,
|
||||||
|
path: ["assetId"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateAssignmentSchema = buildUpdateAssignmentSchema(
|
||||||
|
defaultAssignmentSchemaCopy,
|
||||||
|
)
|
||||||
export type UpdateAssignmentFormType = z.input<typeof updateAssignmentSchema>
|
export type UpdateAssignmentFormType = z.input<typeof updateAssignmentSchema>
|
||||||
export type UpdateAssignmentData = z.output<typeof updateAssignmentSchema>
|
export type UpdateAssignmentData = z.output<typeof updateAssignmentSchema>
|
||||||
|
|
||||||
export const returnAssignmentSchema = z.object({
|
export const returnAssignmentSchema = z.object({
|
||||||
id: z.string().min(1, {
|
id: z.string().min(1, {
|
||||||
error: "Assignment ID is required",
|
error: defaultAssignmentSchemaCopy.idRequired,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
export type ReturnAssignmentFormType = z.infer<typeof returnAssignmentSchema>
|
export type ReturnAssignmentFormType = z.infer<typeof returnAssignmentSchema>
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import { localizeAssignmentFieldErrors } from "@/actions/assignment.messages"
|
||||||
|
|
||||||
|
const actionCopy = {
|
||||||
|
createSuccess: "Asignación creada correctamente",
|
||||||
|
createFailure: "Error al crear la asignación",
|
||||||
|
updateSuccess: "Asignación actualizada correctamente",
|
||||||
|
updateFailure: "Error al actualizar la asignación",
|
||||||
|
returnSuccess: "Asignación devuelta correctamente",
|
||||||
|
returnFailure: "Error al devolver la asignación",
|
||||||
|
notFound: "Asignación no encontrada",
|
||||||
|
itemNotFound: "Artículo no encontrado",
|
||||||
|
itemInsufficientStock: "El artículo no tiene stock suficiente",
|
||||||
|
assetNotFound: "Activo no encontrado",
|
||||||
|
assetItemMismatch: "El activo no pertenece al artículo",
|
||||||
|
assignmentAlreadyReturned: "La asignación ya fue devuelta",
|
||||||
|
invalidData: "Datos de asignación inválidos",
|
||||||
|
genericFailure: "Error al procesar la asignación",
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assignment action message localization", () => {
|
||||||
|
it("localizes known assignment field errors from use-case output", () => {
|
||||||
|
expect(
|
||||||
|
localizeAssignmentFieldErrors(
|
||||||
|
{
|
||||||
|
itemId: ["Item not found"],
|
||||||
|
quantity: ["Item does not have enough stock"],
|
||||||
|
assetId: ["Asset not found"],
|
||||||
|
id: ["Assignment not found", "Assignment already returned"],
|
||||||
|
},
|
||||||
|
actionCopy,
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
itemId: [actionCopy.itemNotFound],
|
||||||
|
quantity: [actionCopy.itemInsufficientStock],
|
||||||
|
assetId: [actionCopy.assetNotFound],
|
||||||
|
id: [actionCopy.notFound, actionCopy.assignmentAlreadyReturned],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("localizes asset-item mismatch and generic data errors", () => {
|
||||||
|
expect(
|
||||||
|
localizeAssignmentFieldErrors(
|
||||||
|
{
|
||||||
|
assetId: ["Asset does not belong to item"],
|
||||||
|
error: ["Invalid assignment data"],
|
||||||
|
},
|
||||||
|
actionCopy,
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
assetId: [actionCopy.assetItemMismatch],
|
||||||
|
error: [actionCopy.invalidData],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("keeps unknown messages unchanged", () => {
|
||||||
|
expect(
|
||||||
|
localizeAssignmentFieldErrors(
|
||||||
|
{ error: ["Unexpected assignment issue"] },
|
||||||
|
actionCopy,
|
||||||
|
),
|
||||||
|
).toEqual({ error: ["Unexpected assignment issue"] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns undefined when no errors are provided", () => {
|
||||||
|
expect(localizeAssignmentFieldErrors(undefined, actionCopy)).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns undefined when errors object is empty", () => {
|
||||||
|
expect(localizeAssignmentFieldErrors({}, actionCopy)).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -448,6 +448,29 @@ describe("i18n dictionaries", () => {
|
|||||||
fallback: {
|
fallback: {
|
||||||
missingValue: "N/A",
|
missingValue: "N/A",
|
||||||
},
|
},
|
||||||
|
actions: {
|
||||||
|
createSuccess: "Assignment created successfully",
|
||||||
|
createFailure: "Error creating assignment",
|
||||||
|
updateSuccess: "Assignment updated successfully",
|
||||||
|
updateFailure: "Error updating assignment",
|
||||||
|
returnSuccess: "Assignment returned successfully",
|
||||||
|
returnFailure: "Error returning assignment",
|
||||||
|
notFound: "Assignment not found",
|
||||||
|
itemNotFound: "Item not found",
|
||||||
|
itemInsufficientStock: "Item does not have enough stock",
|
||||||
|
assetNotFound: "Asset not found",
|
||||||
|
assetItemMismatch: "Asset does not belong to item",
|
||||||
|
assignmentAlreadyReturned: "Assignment already returned",
|
||||||
|
invalidData: "Invalid assignment data",
|
||||||
|
genericFailure: "Error processing assignment",
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
recipientRequired: "Recipient is required",
|
||||||
|
itemIdRequired: "Item is required",
|
||||||
|
quantityMinOne: "Quantity must be at least 1",
|
||||||
|
assetIdRequired: "Asset ID is required when item ID is provided",
|
||||||
|
idRequired: "Assignment ID is required",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getDictionary("es").inventory.assignments).toEqual({
|
expect(getDictionary("es").inventory.assignments).toEqual({
|
||||||
@@ -489,6 +512,30 @@ describe("i18n dictionaries", () => {
|
|||||||
fallback: {
|
fallback: {
|
||||||
missingValue: "No disponible",
|
missingValue: "No disponible",
|
||||||
},
|
},
|
||||||
|
actions: {
|
||||||
|
createSuccess: "Asignación creada correctamente",
|
||||||
|
createFailure: "Error al crear la asignación",
|
||||||
|
updateSuccess: "Asignación actualizada correctamente",
|
||||||
|
updateFailure: "Error al actualizar la asignación",
|
||||||
|
returnSuccess: "Asignación devuelta correctamente",
|
||||||
|
returnFailure: "Error al devolver la asignación",
|
||||||
|
notFound: "Asignación no encontrada",
|
||||||
|
itemNotFound: "Artículo no encontrado",
|
||||||
|
itemInsufficientStock: "El artículo no tiene stock suficiente",
|
||||||
|
assetNotFound: "Activo no encontrado",
|
||||||
|
assetItemMismatch: "El activo no pertenece al artículo",
|
||||||
|
assignmentAlreadyReturned: "La asignación ya fue devuelta",
|
||||||
|
invalidData: "Datos de asignación inválidos",
|
||||||
|
genericFailure: "Error al procesar la asignación",
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
recipientRequired: "El destinatario es obligatorio",
|
||||||
|
itemIdRequired: "El artículo es obligatorio",
|
||||||
|
quantityMinOne: "La cantidad debe ser al menos 1",
|
||||||
|
assetIdRequired:
|
||||||
|
"El activo es obligatorio cuando se especifica el artículo",
|
||||||
|
idRequired: "El ID de asignación es obligatorio",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildCreateAssignmentSchema,
|
||||||
|
buildUpdateAssignmentSchema,
|
||||||
|
} from "@/schemas/assignment.schema"
|
||||||
|
|
||||||
|
const schemaCopy = {
|
||||||
|
recipientRequired: "El destinatario es obligatorio",
|
||||||
|
itemIdRequired: "El artículo es obligatorio",
|
||||||
|
quantityMinOne: "La cantidad debe ser al menos 1",
|
||||||
|
assetIdRequired: "El activo es obligatorio cuando se especifica el artículo",
|
||||||
|
idRequired: "El ID de asignación es obligatorio",
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assignment schema localization", () => {
|
||||||
|
it("uses localized create validation messages for missing required fields", () => {
|
||||||
|
const result = buildCreateAssignmentSchema(schemaCopy).safeParse({
|
||||||
|
recipientId: "",
|
||||||
|
quantity: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
if (!result.success) {
|
||||||
|
const errors = result.error.flatten().fieldErrors
|
||||||
|
|
||||||
|
expect(errors.recipientId).toContain(schemaCopy.recipientRequired)
|
||||||
|
expect(errors.quantity).toContain(schemaCopy.quantityMinOne)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("uses localized update validation messages for missing identifier and invalid item-asset combination", () => {
|
||||||
|
const result = buildUpdateAssignmentSchema(schemaCopy).safeParse({
|
||||||
|
id: "",
|
||||||
|
itemId: "item-1",
|
||||||
|
recipientId: "recipient-1",
|
||||||
|
quantity: 1,
|
||||||
|
assetId: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
if (!result.success) {
|
||||||
|
const errors = result.error.flatten().fieldErrors
|
||||||
|
|
||||||
|
expect(errors.id).toContain(schemaCopy.idRequired)
|
||||||
|
expect(errors.assetId).toContain(schemaCopy.assetIdRequired)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("preserves valid create and update payloads without errors", () => {
|
||||||
|
const createResult = buildCreateAssignmentSchema(schemaCopy).safeParse({
|
||||||
|
recipientId: "recipient-1",
|
||||||
|
itemId: "item-1",
|
||||||
|
quantity: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(createResult.success).toBe(true)
|
||||||
|
if (createResult.success) {
|
||||||
|
expect(createResult.data.recipientId).toBe("recipient-1")
|
||||||
|
expect(createResult.data.quantity).toBe(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateResult = buildUpdateAssignmentSchema(schemaCopy).safeParse({
|
||||||
|
id: "assignment-1",
|
||||||
|
recipientId: "recipient-1",
|
||||||
|
itemId: "item-1",
|
||||||
|
assetId: "asset-1",
|
||||||
|
quantity: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(updateResult.success).toBe(true)
|
||||||
|
if (updateResult.success) {
|
||||||
|
expect(updateResult.data.id).toBe("assignment-1")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("keeps optional assignment fields optional in create", () => {
|
||||||
|
const result = buildCreateAssignmentSchema(schemaCopy).safeParse({
|
||||||
|
recipientId: "recipient-1",
|
||||||
|
quantity: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("allows update without itemId when no assetId is required", () => {
|
||||||
|
const result = buildUpdateAssignmentSchema(schemaCopy).safeParse({
|
||||||
|
id: "assignment-1",
|
||||||
|
recipientId: "recipient-1",
|
||||||
|
quantity: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.data.id).toBe("assignment-1")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user