feat: seed real agents and enable comment templates

This commit is contained in:
esdrasrenan 2025-10-06 20:35:40 -03:00
parent df8c4e29bb
commit 409cbea7b9
13 changed files with 1722 additions and 29 deletions

View file

@ -3,9 +3,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { formatDistanceToNow } from "date-fns"
import { ptBR } from "date-fns/locale"
import { IconLock, IconMessage } from "@tabler/icons-react"
import { IconLock, IconMessage, IconFileText } from "@tabler/icons-react"
import { FileIcon, Image as ImageIcon, PencilLine, Trash2, X } from "lucide-react"
import { useAction, useMutation } from "convex/react"
import { useAction, useMutation, useQuery } from "convex/react"
// @ts-expect-error Convex runtime API lacks TypeScript declarations
import { api } from "@/convex/_generated/api"
import { useAuth } from "@/lib/auth-client"
@ -19,6 +19,7 @@ import { toast } from "sonner"
import { Dropzone } from "@/components/ui/dropzone"
import { RichTextEditor, RichTextContent, sanitizeEditorHtml } from "@/components/ui/rich-text-editor"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"
import { Spinner } from "@/components/ui/spinner"
@ -33,7 +34,7 @@ const submitButtonClass =
"inline-flex items-center gap-2 rounded-lg border border-black bg-black px-3 py-2 text-sm font-semibold text-white transition hover:bg-[#18181b]/85 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#18181b]/30"
export function TicketComments({ ticket }: TicketCommentsProps) {
const { convexUserId } = useAuth()
const { convexUserId, isStaff } = useAuth()
const addComment = useMutation(api.tickets.addComment)
const removeAttachment = useMutation(api.tickets.removeCommentAttachment)
const updateComment = useMutation(api.tickets.updateComment)
@ -48,6 +49,25 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
const [savingCommentId, setSavingCommentId] = useState<string | null>(null)
const [localBodies, setLocalBodies] = useState<Record<string, string>>({})
const templateArgs = convexUserId && isStaff
? { tenantId: ticket.tenantId, viewerId: convexUserId as Id<"users"> }
: "skip"
const templatesResult = useQuery(convexUserId && isStaff ? api.commentTemplates.list : "skip", templateArgs) as
| { id: string; title: string; body: string }[]
| undefined
const templates = templatesResult ?? []
const templatesLoading = Boolean(convexUserId && isStaff) && templatesResult === undefined
const canUseTemplates = Boolean(convexUserId && isStaff)
const insertTemplateIntoBody = (html: string) => {
const sanitized = sanitizeEditorHtml(html)
setBody((current) => {
if (!current) return sanitized
const merged = `${current}<p><br /></p>${sanitized}`
return sanitizeEditorHtml(merged)
})
}
const startEditingComment = useCallback((commentId: string, currentBody: string) => {
setEditingComment({ id: commentId, value: currentBody || "" })
}, [])
@ -352,18 +372,58 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
})}
</div>
) : null}
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 text-xs text-neutral-600">
Visibilidade:
<Select value={visibility} onValueChange={(value) => setVisibility(value as "PUBLIC" | "INTERNAL")}>
<SelectTrigger className={selectTriggerClass}>
<SelectValue placeholder="Visibilidade" />
</SelectTrigger>
<SelectContent className="rounded-lg border border-slate-200 bg-white text-neutral-800 shadow-sm">
<SelectItem value="PUBLIC">Pública</SelectItem>
<SelectItem value="INTERNAL">Interna</SelectItem>
</SelectContent>
</Select>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap items-center gap-3 text-xs text-neutral-600">
{canUseTemplates ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="sm"
className="inline-flex items-center gap-2 border-slate-200 text-sm text-neutral-700 hover:bg-slate-50"
disabled={templatesLoading}
>
<IconFileText className="size-4" />
Inserir template
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-72">
{templatesLoading ? (
<div className="flex items-center gap-2 px-3 py-2 text-sm text-neutral-500">
<Spinner className="size-4" />
Carregando templates...
</div>
) : templates.length === 0 ? (
<div className="px-3 py-2 text-sm text-neutral-500">
Nenhum template disponível. Cadastre novos em configurações.
</div>
) : (
templates.map((template) => (
<DropdownMenuItem
key={template.id}
className="flex flex-col items-start whitespace-normal py-2"
onSelect={() => insertTemplateIntoBody(template.body)}
>
<span className="text-sm font-medium text-neutral-800">{template.title}</span>
</DropdownMenuItem>
))
)}
</DropdownMenuContent>
</DropdownMenu>
) : null}
<div className="flex items-center gap-2">
Visibilidade:
<Select value={visibility} onValueChange={(value) => setVisibility(value as "PUBLIC" | "INTERNAL")}>
<SelectTrigger className={selectTriggerClass}>
<SelectValue placeholder="Visibilidade" />
</SelectTrigger>
<SelectContent className="rounded-lg border border-slate-200 bg-white text-neutral-800 shadow-sm">
<SelectItem value="PUBLIC">Pública</SelectItem>
<SelectItem value="INTERNAL">Interna</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Button type="submit" size="sm" className={submitButtonClass}>
Enviar