"use client" import { useEffect, useMemo, useState } from "react" import { useRouter } from "next/navigation" import { useMutation, useQuery } from "convex/react" import { toast } from "sonner" import type { Doc, Id } from "@/convex/_generated/dataModel" import type { TicketPriority, TicketQueueSummary } from "@/lib/schemas/ticket" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { useAuth } from "@/lib/auth-client" import { api } from "@/convex/_generated/api" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { RichTextEditor, sanitizeEditorHtml } from "@/components/ui/rich-text-editor" import { Spinner } from "@/components/ui/spinner" import { Badge } from "@/components/ui/badge" import { cn } from "@/lib/utils" import { PriorityIcon, priorityBadgeClass, priorityItemClass, priorityStyles, priorityTriggerClass, } from "@/components/tickets/priority-select" import { CategorySelectFields } from "@/components/tickets/category-select" type CustomerOption = { id: string name: string email: string role: string companyId: string | null companyName: string | null companyIsAvulso: boolean avatarUrl: string | null } const ALL_COMPANIES_VALUE = "__all__" const NO_COMPANY_VALUE = "__no_company__" const NO_REQUESTER_VALUE = "__no_requester__" export default function NewTicketPage() { const router = useRouter() const { convexUserId, isStaff, role } = useAuth() const queuesEnabled = Boolean(isStaff && convexUserId) const queueArgs = queuesEnabled ? { tenantId: DEFAULT_TENANT_ID, viewerId: convexUserId as Id<"users"> } : undefined const queuesRemote = useQuery(queuesEnabled ? api.queues.summary : "skip", queueArgs) const queues = useMemo( () => (Array.isArray(queuesRemote) ? (queuesRemote as TicketQueueSummary[]) : []), [queuesRemote] ) const create = useMutation(api.tickets.create) const addComment = useMutation(api.tickets.addComment) const staffRaw = useQuery(api.users.listAgents, { tenantId: DEFAULT_TENANT_ID }) as Doc<"users">[] | undefined const staff = useMemo( () => (staffRaw ?? []).sort((a, b) => a.name.localeCompare(b.name, "pt-BR")), [staffRaw] ) const directoryQueryEnabled = queuesEnabled && Boolean(convexUserId) const companiesArgs = directoryQueryEnabled ? { tenantId: DEFAULT_TENANT_ID, viewerId: convexUserId as Id<"users"> } : undefined const companiesRemote = useQuery( directoryQueryEnabled ? api.companies.list : "skip", companiesArgs ) const companies = useMemo( () => (Array.isArray(companiesRemote) ? companiesRemote : []).map((company) => ({ id: String(company.id), name: company.name, slug: company.slug ?? null, })), [companiesRemote] ) const customersArgs = directoryQueryEnabled ? { tenantId: DEFAULT_TENANT_ID, viewerId: convexUserId as Id<"users"> } : undefined const customersRemote = useQuery( directoryQueryEnabled ? api.users.listCustomers : "skip", customersArgs ) const customers = useMemo( () => (Array.isArray(customersRemote) ? (customersRemote as CustomerOption[]) : []), [customersRemote] ) const [subject, setSubject] = useState("") const [summary, setSummary] = useState("") const [priority, setPriority] = useState("MEDIUM") const [channel, setChannel] = useState("MANUAL") const [queueName, setQueueName] = useState(null) const [assigneeId, setAssigneeId] = useState(null) const [description, setDescription] = useState("") const [loading, setLoading] = useState(false) const [subjectError, setSubjectError] = useState(null) const [categoryId, setCategoryId] = useState(null) const [subcategoryId, setSubcategoryId] = useState(null) const [categoryError, setCategoryError] = useState(null) const [subcategoryError, setSubcategoryError] = useState(null) const [descriptionError, setDescriptionError] = useState(null) const [assigneeInitialized, setAssigneeInitialized] = useState(false) const [companyFilter, setCompanyFilter] = useState(ALL_COMPANIES_VALUE) const [requesterId, setRequesterId] = useState(convexUserId) const [requesterError, setRequesterError] = useState(null) const [customersInitialized, setCustomersInitialized] = useState(false) const queueOptions = useMemo(() => queues.map((q) => q.name), [queues]) const assigneeSelectValue = assigneeId ?? "NONE" const companyOptions = useMemo(() => { const map = new Map() companies.forEach((company) => { map.set(company.id, { id: company.id, name: company.name, isAvulso: false }) }) customers.forEach((customer) => { if (customer.companyId && !map.has(customer.companyId)) { map.set(customer.companyId, { id: customer.companyId, name: customer.companyName ?? "Empresa sem nome", isAvulso: customer.companyIsAvulso, }) } }) const includeNoCompany = customers.some((customer) => !customer.companyId) const result: Array<{ id: string; name: string; isAvulso?: boolean }> = [ { id: ALL_COMPANIES_VALUE, name: "Todas as empresas" }, ] if (includeNoCompany) { result.push({ id: NO_COMPANY_VALUE, name: "Sem empresa" }) } const sorted = Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name, "pt-BR")) return [...result, ...sorted] }, [companies, customers]) const filteredCustomers = useMemo(() => { if (companyFilter === ALL_COMPANIES_VALUE) return customers if (companyFilter === NO_COMPANY_VALUE) { return customers.filter((customer) => !customer.companyId) } return customers.filter((customer) => customer.companyId === companyFilter) }, [companyFilter, customers]) useEffect(() => { if (customersInitialized) return if (!customers.length) return let initialRequester = requesterId if (!initialRequester || !customers.some((customer) => customer.id === initialRequester)) { if (convexUserId && customers.some((customer) => customer.id === convexUserId)) { initialRequester = convexUserId } else { initialRequester = customers[0]?.id ?? null } } if (initialRequester) { setRequesterId(initialRequester) const selected = customers.find((customer) => customer.id === initialRequester) if (selected?.companyId) { setCompanyFilter(selected.companyId) } else if (selected) { setCompanyFilter(NO_COMPANY_VALUE) } } setCustomersInitialized(true) }, [customersInitialized, customers, requesterId, convexUserId]) useEffect(() => { if (!customersInitialized) return const available = filteredCustomers if (available.length === 0) { if (requesterId !== null) { setRequesterId(null) } return } if (!requesterId || !available.some((customer) => customer.id === requesterId)) { setRequesterId(available[0].id) } }, [filteredCustomers, customersInitialized, requesterId]) useEffect(() => { if (requesterId && requesterError) { setRequesterError(null) } }, [requesterId, requesterError]) useEffect(() => { if (assigneeInitialized) return if (!convexUserId) return setAssigneeId(convexUserId) setAssigneeInitialized(true) }, [assigneeInitialized, convexUserId]) // Default queue to "Chamados" if available useEffect(() => { if (queueName) return const hasChamados = queueOptions.includes("Chamados") if (hasChamados) setQueueName("Chamados") }, [queueOptions, queueName]) const allowTicketMentions = useMemo(() => { const normalized = (role ?? "").toLowerCase() return normalized === "admin" || normalized === "agent" || normalized === "collaborator" }, [role]) async function submit(event: React.FormEvent) { event.preventDefault() if (!convexUserId || loading) return const trimmedSubject = subject.trim() if (trimmedSubject.length < 3) { setSubjectError("Informe um assunto com pelo menos 3 caracteres.") return } setSubjectError(null) if (!categoryId) { setCategoryError("Selecione uma categoria.") return } if (!subcategoryId) { setSubcategoryError("Selecione uma categoria secundária.") return } const sanitizedDescription = sanitizeEditorHtml(description) const plainDescription = sanitizedDescription.replace(/<[^>]*>/g, "").trim() if (plainDescription.length === 0) { setDescriptionError("Descreva o contexto do chamado.") return } setDescriptionError(null) if (!requesterId) { setRequesterError("Selecione um solicitante.") toast.error("Selecione um solicitante para o chamado.") return } setLoading(true) toast.loading("Criando ticket...", { id: "create-ticket" }) try { const selQueue = queues.find((q) => q.name === queueName) const queueId = selQueue ? (selQueue.id as Id<"queues">) : undefined const assigneeToSend = assigneeId ? (assigneeId as Id<"users">) : undefined const requesterToSend = requesterId as Id<"users"> const id = await create({ actorId: convexUserId as Id<"users">, tenantId: DEFAULT_TENANT_ID, subject: trimmedSubject, summary: summary.trim() || undefined, priority, channel, queueId, requesterId: requesterToSend, assigneeId: assigneeToSend, categoryId: categoryId as Id<"ticketCategories">, subcategoryId: subcategoryId as Id<"ticketSubcategories">, }) if (plainDescription.length > 0) { await addComment({ ticketId: id as Id<"tickets">, authorId: convexUserId as Id<"users">, visibility: "INTERNAL", body: sanitizedDescription, attachments: [], }) } toast.success("Ticket criado!", { id: "create-ticket" }) router.replace(`/tickets/${id}`) } catch (error) { console.error(error) toast.error("Não foi possível criar o ticket.", { id: "create-ticket" }) } finally { setLoading(false) } } const selectTriggerClass = "flex h-8 w-full items-center justify-between rounded-full border border-slate-300 bg-white px-3 text-sm font-medium text-neutral-800 shadow-sm focus:ring-0 data-[state=open]:border-[#00d6eb]" const selectItemClass = "flex items-center gap-2 rounded-md px-2 py-2 text-sm text-neutral-800 transition data-[state=checked]:bg-[#00e8ff]/15 data-[state=checked]:text-neutral-900 focus:bg-[#00e8ff]/10" return (
Novo ticket Preencha as informações básicas para abrir um chamado.
{ setSubject(event.target.value) if (subjectError) setSubjectError(null) }} placeholder="Ex.: Erro 500 no portal" aria-invalid={subjectError ? "true" : undefined} /> {subjectError ?

{subjectError}

: null}