From 2bc577b501e50f92de97e33b99ac3718c1885186 Mon Sep 17 00:00:00 2001 From: Ruan Bueno Date: Mon, 22 Sep 2025 10:03:21 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20adicionando=20bot=C3=A3o=20de=20editar?= =?UTF-8?q?=20resposta=20no=20forms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[id]/responses/[responseId]/page.tsx | 10 +- .../kanban/_components/response-dialog.tsx | 11 +- src/app/(authenticated)/forms/kanban/page.tsx | 43 ++++++- src/app/(authenticated)/forms/page.tsx | 6 +- src/components/forms/edit-response-button.tsx | 43 +++++++ src/components/forms/edit-response-modal.tsx | 114 ++++++++++++++++++ src/components/forms/form-response.tsx | 42 +++++-- src/components/forms/user-responses-list.tsx | 5 +- src/server/api/routers/form-response.ts | 62 ++++++++++ 9 files changed, 312 insertions(+), 24 deletions(-) create mode 100644 src/components/forms/edit-response-button.tsx create mode 100644 src/components/forms/edit-response-modal.tsx diff --git a/src/app/(authenticated)/forms/[id]/responses/[responseId]/page.tsx b/src/app/(authenticated)/forms/[id]/responses/[responseId]/page.tsx index 0aea2b06..39464cb9 100644 --- a/src/app/(authenticated)/forms/[id]/responses/[responseId]/page.tsx +++ b/src/app/(authenticated)/forms/[id]/responses/[responseId]/page.tsx @@ -10,6 +10,7 @@ import { Badge } from "@/components/ui/badge" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { StatusUpdateButton } from "@/components/forms/status-update-button" import { ResponseDetails } from "@/components/forms/response-details" +import { EditResponseButton } from "@/components/forms/edit-response-button" import { type Field } from "@/lib/form-types" import { DashboardShell } from "@/components/dashboard-shell" import { canAccessForm } from "@/lib/access-control" @@ -47,6 +48,7 @@ export default async function ResponseDetailsPage({ params }: ResponseDetailsPag } const isOwner = response.form.userId === userData?.id + const isAuthor = response.userId === userData?.id function getStatusBadge(status: string) { switch (status) { @@ -135,7 +137,13 @@ export default async function ResponseDetailsPage({ params }: ResponseDetailsPag -
+
+ {isOwner && ( - {response.form.title} +
+ {response.form.title} + +
{getStatusBadge(response.status)} diff --git a/src/app/(authenticated)/forms/kanban/page.tsx b/src/app/(authenticated)/forms/kanban/page.tsx index 9cc2cf1c..f71a9030 100644 --- a/src/app/(authenticated)/forms/kanban/page.tsx +++ b/src/app/(authenticated)/forms/kanban/page.tsx @@ -8,6 +8,7 @@ import type { ResponseStatus } from "@/types/form-responses" import { KanbanColumn } from "./_components/kanban-column" import { ResponseDialog } from "./_components/response-dialog" import type { FormResponse } from "@/types/form-responses" +import { DashboardShell } from "@/components/dashboard-shell" export default function KanbanPage() { const [selectedResponse, setSelectedResponse] = useState(null) @@ -77,15 +78,45 @@ export default function KanbanPage() { if (isLoading) { return ( -
- -
+ +
+ +
+
+ ) + } + + if (!responses || responses.length === 0) { + return ( + +
+

Kanban de Solicitações

+

+ Visualize e organize as respostas recebidas nos seus formulários. +

+
+
+
📋
+

Nenhuma solicitação encontrada

+

+ Você ainda não recebeu nenhuma resposta em seus formulários. +

+

+ As respostas aparecerão aqui quando alguém responder aos seus formulários. +

+
+
) } return ( -
-

Kanban de Solicitações

+ +
+

Kanban de Solicitações

+

+ Visualize e organize as respostas recebidas nos seus formulários. +

+
@@ -115,6 +146,6 @@ export default function KanbanPage() { {selectedResponse && ( )} -
+
) } diff --git a/src/app/(authenticated)/forms/page.tsx b/src/app/(authenticated)/forms/page.tsx index 3a618105..e1129138 100644 --- a/src/app/(authenticated)/forms/page.tsx +++ b/src/app/(authenticated)/forms/page.tsx @@ -62,12 +62,12 @@ export default async function FormsPage() { - + {/* - + */}
diff --git a/src/components/forms/edit-response-button.tsx b/src/components/forms/edit-response-button.tsx new file mode 100644 index 00000000..6b54e75b --- /dev/null +++ b/src/components/forms/edit-response-button.tsx @@ -0,0 +1,43 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Edit } from "lucide-react" +import { EditResponseModal } from "./edit-response-modal" + +interface EditResponseButtonProps { + responseId: string + formId: string + isOwner: boolean + isAuthor: boolean +} + +export function EditResponseButton({ responseId, formId, isOwner, isAuthor }: EditResponseButtonProps) { + const [isModalOpen, setIsModalOpen] = useState(false) + + // Só mostrar o botão se o usuário for o dono do formulário ou o autor da resposta + if (!isOwner && !isAuthor) { + return null + } + + return ( + <> + + + setIsModalOpen(false)} + /> + + ) +} + diff --git a/src/components/forms/edit-response-modal.tsx b/src/components/forms/edit-response-modal.tsx new file mode 100644 index 00000000..caa07a73 --- /dev/null +++ b/src/components/forms/edit-response-modal.tsx @@ -0,0 +1,114 @@ +"use client" + +import { useEffect, useState } from "react" +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { FormResponseComponent } from "./form-response" +import { api } from "@/trpc/react" +import { toast } from "sonner" +import { Button } from "@/components/ui/button" +import { Loader2 } from "lucide-react" +import type { Field } from "@/lib/form-types" +import type { FormResponse } from "@/types/form-responses" + +interface EditResponseModalProps { + responseId: string + formId: string + isOpen: boolean + onClose: () => void +} + +export function EditResponseModal({ responseId, formId, isOpen, onClose }: EditResponseModalProps) { + const [isLoading, setIsLoading] = useState(true) + const [response, setResponse] = useState(null) + const [fields, setFields] = useState([]) + + // Buscar dados da resposta e campos do formulário + const { data: responseData, isLoading: isResponseLoading } = api.formResponse.getById.useQuery( + { responseId }, + { + enabled: isOpen && !!responseId, + } + ) + + const { data: formData } = api.form.getById.useQuery( + { id: formId }, + { + enabled: isOpen && !!formId, + } + ) + + // useEffect para carregar os dados quando o modal abre + useEffect(() => { + if (isOpen && responseData && formData) { + setResponse(responseData) + // @ts-expect-error - JsonValue to Field conversion + setFields(formData.fields as Field[]) + setIsLoading(false) + } else if (isOpen && !isResponseLoading) { + setIsLoading(false) + } + }, [isOpen, responseData, formData, isResponseLoading]) + + // Mutation para atualizar a resposta + const updateResponse = api.formResponse.update.useMutation({ + onSuccess: () => { + toast.success("Resposta atualizada com sucesso!") + onClose() + }, + onError: (error: unknown) => { + const errorMessage = error instanceof Error ? error.message : 'Erro desconhecido' + toast.error(`Erro ao atualizar resposta: ${errorMessage}`) + }, + }) + + const handleSubmit = (data: Record) => { + updateResponse.mutate({ + responseId, + responses: [data] as Record[], + }) + } + + const handleClose = () => { + setIsLoading(true) + setResponse(null) + setFields([]) + onClose() + } + + return ( + + + + Editar Resposta + + Faça as alterações necessárias na resposta do formulário. + + + + {isLoading ? ( +
+ + Carregando dados... +
+ ) : response && fields.length > 0 ? ( + } + onSubmit={handleSubmit} + isEditing={true} + isSubmitting={updateResponse.isPending} + /> + ) : ( +
+

Erro ao carregar dados da resposta.

+ +
+ )} +
+
+ ) +} + diff --git a/src/components/forms/form-response.tsx b/src/components/forms/form-response.tsx index 9457f366..a9350ba0 100644 --- a/src/components/forms/form-response.tsx +++ b/src/components/forms/form-response.tsx @@ -26,9 +26,20 @@ import remarkGfm from "remark-gfm" interface FormResponseComponentProps { formId: string fields: Field[] + existingResponse?: Record + isEditing?: boolean + onSubmit?: (data: Record) => void + isSubmitting?: boolean } -export function FormResponseComponent({ formId, fields }: FormResponseComponentProps) { +export function FormResponseComponent({ + formId, + fields, + existingResponse, + isEditing = false, + onSubmit: customOnSubmit, + isSubmitting: customIsSubmitting +}: FormResponseComponentProps) { const [isSubmitted, setIsSubmitted] = useState(false) const router = useRouter() const {data: form} = api.form.getById.useQuery({id: formId}) @@ -101,7 +112,7 @@ export function FormResponseComponent({ formId, fields }: FormResponseComponentP formState: { errors, isSubmitting }, } = useForm>({ resolver: zodResolver(formSchema), - defaultValues: {}, + defaultValues: existingResponse ?? {}, }) const submitResponse = api.formResponse.create.useMutation({ @@ -134,12 +145,14 @@ export function FormResponseComponent({ formId, fields }: FormResponseComponentP }), ) - submitResponse.mutate({ - formId, - responses: [processedData], - }) - - + if (isEditing && customOnSubmit) { + customOnSubmit(processedData) + } else { + submitResponse.mutate({ + formId, + responses: [processedData], + }) + } } // Função para renderizar mensagens de erro @@ -148,7 +161,7 @@ export function FormResponseComponent({ formId, fields }: FormResponseComponentP return error ?

{JSON.stringify(error.message)}

: null } - if (isSubmitted) { + if (isSubmitted && !isEditing) { return (
@@ -285,8 +298,15 @@ export function FormResponseComponent({ formId, fields }: FormResponseComponentP
))} - ) diff --git a/src/components/forms/user-responses-list.tsx b/src/components/forms/user-responses-list.tsx index 7119cbb3..7690d1f3 100644 --- a/src/components/forms/user-responses-list.tsx +++ b/src/components/forms/user-responses-list.tsx @@ -27,12 +27,13 @@ export function UserResponsesList() { // Fetch responses for both views const { data: responses, isLoading } = api.formResponse.listUserResponses.useQuery() - const { data: kanbanResponses, isLoading: isKanbanLoading, refetch } = api.formResponse.listKanBan.useQuery() + // Para o kanban, usamos a mesma query das respostas do usuário + const { data: kanbanResponses, isLoading: isKanbanLoading, refetch } = api.formResponse.listUserResponses.useQuery() // Update local state when server data changes React.useEffect(() => { if (kanbanResponses) { - setLocalResponses(kanbanResponses) + setLocalResponses(kanbanResponses as FormResponse[]) } }, [kanbanResponses]) diff --git a/src/server/api/routers/form-response.ts b/src/server/api/routers/form-response.ts index 3ce52d8e..3f868490 100644 --- a/src/server/api/routers/form-response.ts +++ b/src/server/api/routers/form-response.ts @@ -224,6 +224,15 @@ export const formResponseRouter = createTRPCRouter({ description: true, }, }, + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true, + imageUrl: true, + }, + }, }, orderBy: { createdAt: "desc", @@ -337,4 +346,57 @@ export const formResponseRouter = createTRPCRouter({ return response }), + + update: protectedProcedure + .input( + z.object({ + responseId: z.string(), + responses: z.array(z.record(z.string(), z.any())), + }), + ) + .mutation(async ({ ctx, input }) => { + // Verificar se a resposta existe + const existingResponse = await ctx.db.formResponse.findUnique({ + where: { id: input.responseId }, + include: { + form: { select: { userId: true } }, + user: { select: { id: true } }, + }, + }) + + if (!existingResponse) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Resposta não encontrada", + }) + } + + // Verificar se o usuário é o dono do formulário ou o autor da resposta + if (existingResponse.form.userId !== ctx.auth.userId && existingResponse.userId !== ctx.auth.userId) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Você não tem permissão para editar esta resposta", + }) + } + + return await ctx.db.formResponse.update({ + where: { id: input.responseId }, + data: { + responses: input.responses as unknown as InputJsonValue[], + updatedAt: new Date(), + }, + include: { + form: true, + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true, + imageUrl: true, + }, + }, + }, + }) + }), })