diff --git a/src/actions/assignment.actions.ts b/src/actions/assignment.actions.ts index f9181a6..4c50ba4 100644 --- a/src/actions/assignment.actions.ts +++ b/src/actions/assignment.actions.ts @@ -6,6 +6,7 @@ import { localizeAssignmentFieldErrors } from "@/actions/assignment.messages" import { getI18n } from "@/i18n/server" import { buildCreateAssignmentSchema, + buildReturnAssignmentSchema, buildUpdateAssignmentSchema, type CreateAssignmentFormType, type ReturnAssignmentFormType, @@ -120,14 +121,32 @@ export async function updateAssignment(formData: UpdateAssignmentFormType) { } } -export async function returnAssignment(formData: ReturnAssignmentFormType) { +type ReturnAssignmentActionResult = + | { success: true; message: string } + | { success: false; errors?: Record; message?: string } + +export async function returnAssignment( + formData: ReturnAssignmentFormType, +): Promise { const { dictionary } = await getI18n() const copy = dictionary.inventory.assignments const userId = await getAuthenticatedUserId() + const validatedFields = buildReturnAssignmentSchema(copy.schema).safeParse( + formData, + ) + + if (!validatedFields.success) { + return { + success: false, + errors: flattenError(validatedFields.error).fieldErrors, + } + } + const result = await returnAssignmentUseCase({ - id: formData.id, + id: validatedFields.data.id, actorId: userId, + returns: validatedFields.data.returns, }) if (!result.success) { diff --git a/src/app/(dashboard)/assignments/_components/return.button.tsx b/src/app/(dashboard)/assignments/_components/return.button.tsx index 530d471..b5f6bb3 100644 --- a/src/app/(dashboard)/assignments/_components/return.button.tsx +++ b/src/app/(dashboard)/assignments/_components/return.button.tsx @@ -2,53 +2,219 @@ import { ArrowLeft } from "lucide-react" import { useRouter } from "next/navigation" -import { useTransition } from "react" +import { useState, useTransition } from "react" import { toast } from "sonner" import { returnAssignment } from "@/actions/assignment.actions" import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" import type { ReturnAssignmentFormType } from "@/schemas/assignment.schema" +type PartialReturnCopy = { + title: string + quantity: string + quantityPlaceholder: string + notes: string + notesPlaceholder: string + submit: string + cancel: string + maxQuantity: string + errorConcurrent: string + errorGeneric: string +} + +const defaultPartialReturnCopy: PartialReturnCopy = { + title: "Devolver artículo", + quantity: "Cantidad", + quantityPlaceholder: "1", + notes: "Notas", + notesPlaceholder: "Notas opcionales", + submit: "Devolver", + cancel: "Cancelar", + maxQuantity: "Máximo: {max}", + errorConcurrent: + "La devolución fue modificada por otro usuario. Recarga e inténtalo de nuevo.", + errorGeneric: "Error al procesar la devolución", +} + export default function ReturnButton({ assignmentId, ariaLabel, + assignmentLineId, + remainingQuantity, + copy = defaultPartialReturnCopy, }: { assignmentId: string ariaLabel: string + assignmentLineId?: string + remainingQuantity?: number + copy?: PartialReturnCopy }) { const router = useRouter() const [isPending, startTransition] = useTransition() + const [open, setOpen] = useState(false) + const [quantity, setQuantity] = useState(1) + const [notes, setNotes] = useState("") + const [errorKey, setErrorKey] = useState< + "errorConcurrent" | "errorGeneric" | null + >(null) + + const isQuantityMode = + assignmentLineId !== undefined && remainingQuantity !== undefined + const isOverMax = isQuantityMode && quantity > remainingQuantity + const canSubmit = isQuantityMode + ? quantity >= 1 && quantity <= remainingQuantity && !isPending + : !isPending + + const handleOpenChange = (nextOpen: boolean) => { + setOpen(nextOpen) + if (!nextOpen) { + setQuantity(1) + setNotes("") + setErrorKey(null) + } + } + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault() + if (!canSubmit) return + + setErrorKey(null) + + const formData: ReturnAssignmentFormType = isQuantityMode + ? { + id: assignmentId, + returns: [ + { + assignmentLineId, + quantity, + notes: notes.trim() || undefined, + }, + ], + } + : { id: assignmentId } - const handleReturn = (formData: ReturnAssignmentFormType) => { startTransition(async () => { const response = await returnAssignment(formData) - if (!response.success && response.errors?.id) { - toast.error(response.errors.id[0]) + if (response.success) { + setOpen(false) + setQuantity(1) + setNotes("") + toast.success(response.message) + router.refresh() return } - if (response.success) { - toast.success(response.message) - router.refresh() + if (response.errors?.error?.includes("errorConcurrent")) { + setErrorKey("errorConcurrent") } else { - toast.error(response.message ?? "Unknown error") + setErrorKey("errorGeneric") } }) } return ( -
handleReturn({ id: assignmentId })} className="w-full"> - - -
+ + + + + +
+ + {copy.title} + + {copy.title} + + + +
+ {isQuantityMode && ( + <> +
+ + + setQuantity(Number(event.target.value)) + } + placeholder={copy.quantityPlaceholder} + aria-invalid={isOverMax || undefined} + /> + {isOverMax && ( +

+ {copy.maxQuantity.replace( + "{max}", + String(remainingQuantity), + )} +

+ )} +
+
+ +