"use client"
import { useMemo, useState } from "react"
import { useRouter } from "next/navigation"
import { useMutation } from "convex/react"
import { toast } from "sonner"
import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel"
import { DEFAULT_TENANT_ID } from "@/lib/constants"
import type { TicketPriority } from "@/lib/schemas/ticket"
import { sanitizeEditorHtml } from "@/components/ui/rich-text-editor"
import { useAuth } from "@/lib/auth-client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { CategorySelectFields } from "@/components/tickets/category-select"
import { Dropzone } from "@/components/ui/dropzone"
import { RichTextEditor } from "@/components/ui/rich-text-editor"
const DEFAULT_PRIORITY: TicketPriority = "MEDIUM"
function toHtml(text: string) {
const escaped = text
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'")
return `
${escaped.replace(/\n/g, "
")}
`
}
export function PortalTicketForm() {
const router = useRouter()
const { convexUserId, session, machineContext, machineContextError, machineContextLoading } = useAuth()
const createTicket = useMutation(api.tickets.create)
const addComment = useMutation(api.tickets.addComment)
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
const viewerId = (convexUserId ?? machineContext?.assignedUserId ?? null) as Id<"users"> | null
const [subject, setSubject] = useState("")
const [summary, setSummary] = useState("")
const [description, setDescription] = useState("")
const [categoryId, setCategoryId] = useState(null)
const [subcategoryId, setSubcategoryId] = useState(null)
const [attachments, setAttachments] = useState>([])
const attachmentsTotalBytes = useMemo(
() => attachments.reduce((acc, item) => acc + (item.size ?? 0), 0),
[attachments]
)
const [isSubmitting, setIsSubmitting] = useState(false)
const machineInactive = machineContext?.isActive === false
const isFormValid = useMemo(() => {
return Boolean(subject.trim() && description.trim() && categoryId && subcategoryId && !machineInactive)
}, [subject, description, categoryId, subcategoryId, machineInactive])
const isViewerReady = Boolean(viewerId)
const viewerErrorMessage = useMemo(() => {
if (!machineContextError) return null
const suffix = machineContextError.status ? ` (status ${machineContextError.status})` : ""
return `${machineContextError.message}${suffix}`
}, [machineContextError])
async function handleSubmit(event: React.FormEvent) {
event.preventDefault()
if (isSubmitting || !isFormValid) return
if (machineInactive) {
toast.error("Esta máquina está desativada no momento. Reative-a para abrir novos chamados.", { id: "portal-new-ticket" })
return
}
if (!viewerId) {
const detail = viewerErrorMessage ? ` Detalhes: ${viewerErrorMessage}` : ""
toast.error(
`Não foi possível identificar o colaborador vinculado a esta máquina. Tente abrir novamente o portal ou contate o suporte.${detail}`,
{ id: "portal-new-ticket" }
)
return
}
const trimmedSubject = subject.trim()
const trimmedSummary = summary.trim()
const sanitizedDescription = sanitizeEditorHtml(description || "")
const plainDescription = sanitizedDescription.replace(/<[^>]*>/g, "").trim()
if (plainDescription.length === 0) {
toast.error("Descreva o que aconteceu para que possamos ajudar melhor.", { id: "portal-new-ticket" })
return
}
setIsSubmitting(true)
toast.loading("Abrindo chamado...", { id: "portal-new-ticket" })
try {
const id = await createTicket({
actorId: viewerId,
tenantId,
subject: trimmedSubject,
summary: trimmedSummary || undefined,
priority: DEFAULT_PRIORITY,
channel: "MANUAL",
queueId: undefined,
requesterId: viewerId,
categoryId: categoryId as Id<"ticketCategories">,
subcategoryId: subcategoryId as Id<"ticketSubcategories">,
})
if (plainDescription.length > 0) {
const MAX_COMMENT_CHARS = 20000
if (plainDescription.length > MAX_COMMENT_CHARS) {
toast.error(`Descrição muito longa (máx. ${MAX_COMMENT_CHARS} caracteres)`, { id: "portal-new-ticket" })
setIsSubmitting(false)
return
}
const htmlBody = sanitizedDescription || toHtml(trimmedSummary || trimmedSubject)
const typedAttachments = attachments.map((file) => ({
storageId: file.storageId as Id<"_storage">,
name: file.name,
size: file.size,
type: file.type,
}))
await addComment({
ticketId: id as Id<"tickets">,
authorId: viewerId,
visibility: "PUBLIC",
body: htmlBody,
attachments: typedAttachments,
})
}
toast.success("Chamado criado com sucesso!", { id: "portal-new-ticket" })
setAttachments([])
router.replace(`/portal/tickets/${id}`)
} catch (error) {
console.error(error)
toast.error("Não foi possível abrir o chamado.", { id: "portal-new-ticket" })
} finally {
setIsSubmitting(false)
}
}
return (
Abrir novo chamado
{machineInactive ? (
Esta máquina foi desativada pelos administradores e não pode abrir novos chamados até ser reativada.
) : null}
{!isViewerReady ? (
Vincule esta máquina a um colaborador na aplicação desktop para enviar chamados em nome dele.
{machineContextLoading ? (
Carregando informa��es da m�quina...
) : null}
{viewerErrorMessage ? (
Detalhes do erro: {viewerErrorMessage}
) : null}
) : null}
)
}