Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -135,7 +137,13 @@ export default async function ResponseDetailsPage({ params }: ResponseDetailsPag
</CardContent>
</Card>

<div className="flex justify-end">
<div className="flex justify-end gap-2">
<EditResponseButton
responseId={response.id}
formId={id}
isOwner={isOwner}
isAuthor={isAuthor}
/>
{isOwner && (
<StatusUpdateButton
responseId={response.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Separator } from "@/components/ui/separator"
import { format } from "date-fns"
import { ptBR } from "date-fns/locale"
import { ResponseDetails } from "@/components/forms/response-details"
import { EditResponseButton } from "@/components/forms/edit-response-button"
import { type Field } from "@/lib/form-types"

interface ResponseDialogProps {
Expand Down Expand Up @@ -99,7 +100,15 @@ export function ResponseDialog({ responseId, open, onOpenChange }: ResponseDialo
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px] max-h-dvh overflow-y-auto">
<DialogHeader>
<DialogTitle>{response.form.title}</DialogTitle>
<div className="flex items-center justify-between">
<DialogTitle>{response.form.title}</DialogTitle>
<EditResponseButton
responseId={responseId}
formId={response.formId}
isOwner={true} // Na página kanban, o usuário é dono dos formulários
isAuthor={false}
/>
</div>
<DialogDescription className="flex items-center gap-2">
{getStatusBadge(response.status)}
<span className="text-sm text-gray-500">
Expand Down
43 changes: 37 additions & 6 deletions src/app/(authenticated)/forms/kanban/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null)
Expand Down Expand Up @@ -77,15 +78,45 @@ export default function KanbanPage() {

if (isLoading) {
return (
<div className="flex h-screen items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
<DashboardShell>
<div className="flex h-64 items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
</DashboardShell>
)
}

if (!responses || responses.length === 0) {
return (
<DashboardShell>
<div className="mb-8">
<h1 className="text-3xl font-bold tracking-tight">Kanban de Solicitações</h1>
<p className="text-muted-foreground mt-2">
Visualize e organize as respostas recebidas nos seus formulários.
</p>
</div>
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="text-6xl mb-4">📋</div>
<h3 className="text-lg font-medium">Nenhuma solicitação encontrada</h3>
<p className="text-muted-foreground mt-1 mb-4">
Você ainda não recebeu nenhuma resposta em seus formulários.
</p>
<p className="text-sm text-muted-foreground">
As respostas aparecerão aqui quando alguém responder aos seus formulários.
</p>
</div>
</DashboardShell>
)
}

return (
<div className="container mx-auto p-4">
<h1 className="mb-6 text-3xl font-bold">Kanban de Solicitações</h1>
<DashboardShell>
<div className="mb-8">
<h1 className="text-3xl font-bold tracking-tight">Kanban de Solicitações</h1>
<p className="text-muted-foreground mt-2">
Visualize e organize as respostas recebidas nos seus formulários.
</p>
</div>

<DragDropContext onDragEnd={onDragEnd}>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
Expand Down Expand Up @@ -115,6 +146,6 @@ export default function KanbanPage() {
{selectedResponse && (
<ResponseDialog responseId={selectedResponse} open={isDialogOpen} onOpenChange={setIsDialogOpen} />
)}
</div>
</DashboardShell>
)
}
6 changes: 3 additions & 3 deletions src/app/(authenticated)/forms/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ export default async function FormsPage() {
</DialogFooter>
</DialogContent>
</Dialog>
<Link href="/forms/kanban">
{/* <Link href="/forms/kanban">
<Button className="w-full" variant='outline'>
<LucideKanbanSquare className="mr-2 h-4 w-4" />
<LucideKanbanSquare className="mr-2 h-4 w-4" /> -> Lógica adaptada em "Minhas respostas"
Kanban
</Button>
</Link>
</Link> */}
</div>
</div>
<div className="flex gap-3 flex-col md:flex-row w-full md:w-auto">
Expand Down
43 changes: 43 additions & 0 deletions src/components/forms/edit-response-button.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Button
variant="outline"
size="sm"
onClick={() => setIsModalOpen(true)}
>
<Edit className="h-4 w-4 mr-1" />
Editar Resposta
</Button>

<EditResponseModal
responseId={responseId}
formId={formId}
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
/>
</>
)
}

114 changes: 114 additions & 0 deletions src/components/forms/edit-response-modal.tsx
Original file line number Diff line number Diff line change
@@ -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<FormResponse | null>(null)
const [fields, setFields] = useState<Field[]>([])

// 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<string, unknown>) => {
updateResponse.mutate({
responseId,
responses: [data] as Record<string, unknown>[],
})
}

const handleClose = () => {
setIsLoading(true)
setResponse(null)
setFields([])
onClose()
}

return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Editar Resposta</DialogTitle>
<DialogDescription>
Faça as alterações necessárias na resposta do formulário.
</DialogDescription>
</DialogHeader>

{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin" />
<span className="ml-2">Carregando dados...</span>
</div>
) : response && fields.length > 0 ? (
<FormResponseComponent
formId={formId}
fields={fields}
existingResponse={response.responses[0] as Record<string, unknown>}
onSubmit={handleSubmit}
isEditing={true}
isSubmitting={updateResponse.isPending}
/>
) : (
<div className="flex flex-col items-center justify-center py-8">
<p className="text-muted-foreground">Erro ao carregar dados da resposta.</p>
<Button variant="outline" onClick={handleClose} className="mt-4">
Fechar
</Button>
</div>
)}
</DialogContent>
</Dialog>
)
}

42 changes: 31 additions & 11 deletions src/components/forms/form-response.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,20 @@ import remarkGfm from "remark-gfm"
interface FormResponseComponentProps {
formId: string
fields: Field[]
existingResponse?: Record<string, unknown>
isEditing?: boolean
onSubmit?: (data: Record<string, unknown>) => 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})
Expand Down Expand Up @@ -101,7 +112,7 @@ export function FormResponseComponent({ formId, fields }: FormResponseComponentP
formState: { errors, isSubmitting },
} = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {},
defaultValues: existingResponse ?? {},
})

const submitResponse = api.formResponse.create.useMutation({
Expand Down Expand Up @@ -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
Expand All @@ -148,7 +161,7 @@ export function FormResponseComponent({ formId, fields }: FormResponseComponentP
return error ? <p className="text-sm font-medium text-destructive mt-1">{JSON.stringify(error.message)}</p> : null
}

if (isSubmitted) {
if (isSubmitted && !isEditing) {
return (
<div className="space-y-6">
<Alert className="bg-success/10 border-success">
Expand Down Expand Up @@ -285,8 +298,15 @@ export function FormResponseComponent({ formId, fields }: FormResponseComponentP
</div>
))}

<Button type="submit" className="mt-6" disabled={isSubmitting}>
{isSubmitting ? "Enviando..." : "Enviar Resposta"}
<Button
type="submit"
className="mt-6"
disabled={customIsSubmitting ?? isSubmitting}
>
{customIsSubmitting !== undefined
? (customIsSubmitting ? "Salvando..." : "Salvar Alterações")
: (isSubmitting ? "Enviando..." : (isEditing ? "Salvar Alterações" : "Enviar Resposta"))
}
Comment on lines +301 to +309

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Disable submit when editing without handler; keep labels consistent.

Prevents accidental submission path and improves UX text.

       <Button
         type="submit"
         className="mt-6"
-        disabled={customIsSubmitting ?? isSubmitting}
+        disabled={(customIsSubmitting ?? isSubmitting) || (isEditing && !customOnSubmit)}
       >
-        {customIsSubmitting !== undefined
-          ? (customIsSubmitting ? "Salvando..." : "Salvar Alterações")
-          : (isSubmitting ? "Enviando..." : (isEditing ? "Salvar Alterações" : "Enviar Resposta"))
-        }
+        {customIsSubmitting !== undefined
+          ? customIsSubmitting
+            ? "Salvando..."
+            : "Salvar Alterações"
+          : isSubmitting
+            ? "Enviando..."
+            : isEditing
+              ? (customOnSubmit ? "Salvar Alterações" : "Indisponível")
+              : "Enviar Resposta"}
       </Button>

Committable suggestion skipped: line range outside the PR's diff.

</Button>
</form>
)
Expand Down
5 changes: 3 additions & 2 deletions src/components/forms/user-responses-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down
Loading
Loading