feat: dispositivos e ajustes de csat e relatórios
This commit is contained in:
parent
25d2a9b062
commit
e0ef66555d
86 changed files with 5811 additions and 992 deletions
|
|
@ -13,6 +13,7 @@ import { Checkbox } from "@/components/ui/checkbox"
|
|||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
|
||||
type ClosingTemplate = { id: string; title: string; body: string }
|
||||
|
||||
|
|
@ -45,7 +46,7 @@ const DEFAULT_CLOSING_TEMPLATES: ClosingTemplate[] = [
|
|||
body: sanitizeTemplate(`
|
||||
<p>Olá {{cliente}},</p>
|
||||
<p>A equipe da ${DEFAULT_COMPANY_NAME} agradece o contato. Este ticket está sendo encerrado.</p>
|
||||
<p>Se surgirem novas questões, você pode reabrir o ticket em até 7 dias ou nos contatar pelo número <strong>${DEFAULT_PHONE_NUMBER}</strong>. Obrigado.</p>
|
||||
<p>Se surgirem novas questões, você pode reabrir o ticket em até 14 dias ou nos contatar pelo número <strong>${DEFAULT_PHONE_NUMBER}</strong>. Obrigado.</p>
|
||||
<p>{{agente}} · ${DEFAULT_COMPANY_NAME}</p>
|
||||
`),
|
||||
},
|
||||
|
|
@ -67,7 +68,7 @@ const DEFAULT_CLOSING_TEMPLATES: ClosingTemplate[] = [
|
|||
body: sanitizeTemplate(`
|
||||
<p>Prezado(a) {{cliente}},</p>
|
||||
<p>Esse ticket está sendo encerrado pois realizamos 3 tentativas sem retorno.</p>
|
||||
<p>Você pode reabrir este ticket em até 7 dias ou entrar em contato pelo telefone <strong>${DEFAULT_PHONE_NUMBER}</strong> quando preferir.</p>
|
||||
<p>Você pode reabrir este ticket em até 14 dias ou entrar em contato pelo telefone <strong>${DEFAULT_PHONE_NUMBER}</strong> quando preferir.</p>
|
||||
<p>{{agente}} · ${DEFAULT_COMPANY_NAME}</p>
|
||||
`),
|
||||
},
|
||||
|
|
@ -105,6 +106,7 @@ export function CloseTicketDialog({
|
|||
ticketId,
|
||||
tenantId,
|
||||
actorId,
|
||||
ticketReference,
|
||||
requesterName,
|
||||
agentName,
|
||||
onSuccess,
|
||||
|
|
@ -117,6 +119,7 @@ export function CloseTicketDialog({
|
|||
ticketId: string
|
||||
tenantId: string
|
||||
actorId: Id<"users"> | null
|
||||
ticketReference?: number | null
|
||||
requesterName?: string | null
|
||||
agentName?: string | null
|
||||
onSuccess: () => void
|
||||
|
|
@ -128,7 +131,7 @@ export function CloseTicketDialog({
|
|||
onWorkSummaryAdjusted?: (result: AdjustWorkSummaryResult) => void
|
||||
canAdjustTime?: boolean
|
||||
}) {
|
||||
const updateStatus = useMutation(api.tickets.updateStatus)
|
||||
const resolveTicketMutation = useMutation(api.tickets.resolveTicket)
|
||||
const addComment = useMutation(api.tickets.addComment)
|
||||
const adjustWorkSummary = useMutation(api.tickets.adjustWorkSummary)
|
||||
|
||||
|
|
@ -160,6 +163,24 @@ export function CloseTicketDialog({
|
|||
const [externalMinutes, setExternalMinutes] = useState<string>("0")
|
||||
const [adjustReason, setAdjustReason] = useState<string>("")
|
||||
const enableAdjustment = Boolean(canAdjustTime && workSummary)
|
||||
const [linkedReference, setLinkedReference] = useState<string>("")
|
||||
const [reopenWindowDays, setReopenWindowDays] = useState<string>("14")
|
||||
|
||||
const normalizedReference = useMemo(() => {
|
||||
const digits = linkedReference.replace(/[^0-9]/g, "").trim()
|
||||
if (!digits) return null
|
||||
const parsed = Number(digits)
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return null
|
||||
if (ticketReference && parsed === ticketReference) return null
|
||||
return parsed
|
||||
}, [linkedReference, ticketReference])
|
||||
|
||||
const linkedTicket = useQuery(
|
||||
api.tickets.findByReference,
|
||||
actorId && normalizedReference ? { tenantId, viewerId: actorId, reference: normalizedReference } : "skip"
|
||||
) as { id: Id<"tickets">; reference: number; subject: string; status: string } | null | undefined
|
||||
const isLinkLoading = Boolean(actorId && normalizedReference && linkedTicket === undefined)
|
||||
const linkNotFound = Boolean(normalizedReference && linkedTicket === null && !isLinkLoading)
|
||||
|
||||
const hydrateTemplateBody = useCallback((templateHtml: string) => {
|
||||
const withPlaceholders = applyTemplatePlaceholders(templateHtml, requesterName, agentName)
|
||||
|
|
@ -269,6 +290,21 @@ export function CloseTicketDialog({
|
|||
setIsSubmitting(true)
|
||||
toast.loading(applyAdjustment ? "Ajustando tempo e encerrando ticket..." : "Encerrando ticket...", { id: "close-ticket" })
|
||||
try {
|
||||
if (linkedReference.trim().length > 0) {
|
||||
if (isLinkLoading) {
|
||||
toast.error("Aguarde carregar o ticket vinculado antes de encerrar.", { id: "close-ticket" })
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
if (linkNotFound || !linkedTicket) {
|
||||
toast.error("Não encontramos o ticket informado para vincular. Verifique o número e tente novamente.", {
|
||||
id: "close-ticket",
|
||||
})
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (applyAdjustment) {
|
||||
const result = (await adjustWorkSummary({
|
||||
ticketId: ticketId as unknown as Id<"tickets">,
|
||||
|
|
@ -280,7 +316,13 @@ export function CloseTicketDialog({
|
|||
onWorkSummaryAdjusted?.(result)
|
||||
}
|
||||
|
||||
await updateStatus({ ticketId: ticketId as unknown as Id<"tickets">, status: "RESOLVED", actorId })
|
||||
const reopenDaysNumber = Number(reopenWindowDays)
|
||||
await resolveTicketMutation({
|
||||
ticketId: ticketId as unknown as Id<"tickets">,
|
||||
actorId,
|
||||
resolvedWithTicketId: linkedTicket ? (linkedTicket.id as Id<"tickets">) : undefined,
|
||||
reopenWindowDays: Number.isFinite(reopenDaysNumber) ? reopenDaysNumber : undefined,
|
||||
})
|
||||
const withPlaceholders = applyTemplatePlaceholders(message, requesterName, agentName)
|
||||
const sanitized = stripLeadingEmptyParagraphs(sanitizeEditorHtml(withPlaceholders))
|
||||
const hasContent = sanitized.replace(/<[^>]*>/g, "").trim().length > 0
|
||||
|
|
@ -351,6 +393,50 @@ export function CloseTicketDialog({
|
|||
/>
|
||||
<p className="text-xs text-neutral-500">Você pode editar o conteúdo antes de enviar. Deixe em branco para encerrar sem comentário adicional.</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-4">
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="linked-reference" className="text-sm font-medium text-neutral-800">
|
||||
Ticket relacionado (opcional)
|
||||
</Label>
|
||||
<Input
|
||||
id="linked-reference"
|
||||
value={linkedReference}
|
||||
onChange={(event) => setLinkedReference(event.target.value)}
|
||||
placeholder="Número do ticket relacionado (ex.: 12345)"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
{linkedReference.trim().length === 0 ? (
|
||||
<p className="text-xs text-neutral-500">Informe o número de outro ticket quando o atendimento estiver relacionado.</p>
|
||||
) : isLinkLoading ? (
|
||||
<p className="flex items-center gap-2 text-xs text-neutral-500">
|
||||
<Spinner className="size-3" /> Procurando ticket #{normalizedReference}...
|
||||
</p>
|
||||
) : linkNotFound ? (
|
||||
<p className="text-xs text-red-500">Ticket não encontrado ou sem acesso permitido. Verifique o número informado.</p>
|
||||
) : linkedTicket ? (
|
||||
<p className="text-xs text-emerald-600">
|
||||
Será registrado vínculo com o ticket #{linkedTicket.reference} — {linkedTicket.subject ?? "Sem assunto"}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reopen-window" className="text-sm font-medium text-neutral-800">
|
||||
Reabertura permitida
|
||||
</Label>
|
||||
<Select value={reopenWindowDays} onValueChange={setReopenWindowDays} disabled={isSubmitting}>
|
||||
<SelectTrigger id="reopen-window">
|
||||
<SelectValue placeholder="Escolha o prazo" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="7">7 dias</SelectItem>
|
||||
<SelectItem value="14">14 dias</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-neutral-500">Após esse período o ticket não poderá ser reaberto automaticamente.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{enableAdjustment ? (
|
||||
<div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-4">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue