ui: keep close dialog content unchanged; refactor to shared component; layout tweaks per request
This commit is contained in:
parent
040965b148
commit
741a0b5b70
3 changed files with 249 additions and 171 deletions
226
src/components/tickets/close-ticket-dialog.tsx
Normal file
226
src/components/tickets/close-ticket-dialog.tsx
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { useMutation, useQuery } from "convex/react"
|
||||||
|
import { api } from "@/convex/_generated/api"
|
||||||
|
import type { Id } from "@/convex/_generated/dataModel"
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Spinner } from "@/components/ui/spinner"
|
||||||
|
import { RichTextEditor, sanitizeEditorHtml } from "@/components/ui/rich-text-editor"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
|
||||||
|
type ClosingTemplate = { id: string; title: string; body: string }
|
||||||
|
|
||||||
|
const DEFAULT_PHONE_NUMBER = "(11) 4173-5368"
|
||||||
|
|
||||||
|
const DEFAULT_CLOSING_TEMPLATES: ClosingTemplate[] = [
|
||||||
|
{
|
||||||
|
id: "default-standard",
|
||||||
|
title: "Encerramento padrão",
|
||||||
|
body: sanitizeEditorHtml(`
|
||||||
|
<p>Olá {{cliente}},</p>
|
||||||
|
<p>A equipe da Raven 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>👍 👀 🙌<br />Gabriel Henrique · Raven</p>
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "default-no-contact",
|
||||||
|
title: "Tentativa de contato sem sucesso",
|
||||||
|
body: sanitizeEditorHtml(`
|
||||||
|
<p>Prezado(a) {{cliente}},</p>
|
||||||
|
<p>Realizamos uma tentativa de contato, mas não obtivemos sucesso.</p>
|
||||||
|
<p>Por favor, retorne assim que possível para seguirmos com as verificações necessárias.</p>
|
||||||
|
<p>Este ticket será encerrado após 3 tentativas realizadas sem sucesso.</p>
|
||||||
|
<p>Telefone para contato: <strong>${DEFAULT_PHONE_NUMBER}</strong>.</p>
|
||||||
|
<p>👍 👀 🙌<br />Gabriel Henrique · Raven</p>
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "default-closed-after-attempts",
|
||||||
|
title: "Encerramento após 3 tentativas",
|
||||||
|
body: sanitizeEditorHtml(`
|
||||||
|
<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>👍 👀 🙌<br />Gabriel Henrique · Raven</p>
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function applyTemplatePlaceholders(html: string, customerName?: string | null) {
|
||||||
|
const normalizedName = customerName?.trim()
|
||||||
|
const fallback = normalizedName && normalizedName.length > 0 ? normalizedName : "cliente"
|
||||||
|
return html.replace(/{{\s*(cliente|customer|customername|nome|nomecliente)\s*}}/gi, fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CloseTicketDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
ticketId,
|
||||||
|
tenantId,
|
||||||
|
actorId,
|
||||||
|
requesterName,
|
||||||
|
onSuccess,
|
||||||
|
}: {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
ticketId: string
|
||||||
|
tenantId: string
|
||||||
|
actorId: Id<"users"> | null
|
||||||
|
requesterName?: string | null
|
||||||
|
onSuccess: () => void
|
||||||
|
}) {
|
||||||
|
const updateStatus = useMutation(api.tickets.updateStatus)
|
||||||
|
const addComment = useMutation(api.tickets.addComment)
|
||||||
|
|
||||||
|
const closingTemplates = useQuery(
|
||||||
|
actorId && open ? api.commentTemplates.list : "skip",
|
||||||
|
actorId && open ? { tenantId, viewerId: actorId, kind: "closing" as const } : "skip"
|
||||||
|
) as { id: string; title: string; body: string }[] | undefined
|
||||||
|
|
||||||
|
const templatesLoading = Boolean(actorId && open && closingTemplates === undefined)
|
||||||
|
const templates = useMemo<ClosingTemplate[]>(() => {
|
||||||
|
if (closingTemplates && closingTemplates.length > 0) {
|
||||||
|
return closingTemplates.map((t) => ({ id: t.id, title: t.title, body: t.body }))
|
||||||
|
}
|
||||||
|
return DEFAULT_CLOSING_TEMPLATES
|
||||||
|
}, [closingTemplates])
|
||||||
|
|
||||||
|
const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(null)
|
||||||
|
const [message, setMessage] = useState<string>("")
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setSelectedTemplateId(null)
|
||||||
|
setMessage("")
|
||||||
|
setIsSubmitting(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (templates.length > 0 && !selectedTemplateId && !message) {
|
||||||
|
const first = templates[0]
|
||||||
|
const hydrated = sanitizeEditorHtml(applyTemplatePlaceholders(first.body, requesterName))
|
||||||
|
setSelectedTemplateId(first.id)
|
||||||
|
setMessage(hydrated)
|
||||||
|
}
|
||||||
|
}, [open, templates, requesterName, selectedTemplateId, message])
|
||||||
|
|
||||||
|
const handleTemplateSelect = (template: ClosingTemplate) => {
|
||||||
|
setSelectedTemplateId(template.id)
|
||||||
|
const filled = sanitizeEditorHtml(applyTemplatePlaceholders(template.body, requesterName))
|
||||||
|
setMessage(filled)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!actorId) {
|
||||||
|
toast.error("É necessário estar autenticado para encerrar o ticket.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setIsSubmitting(true)
|
||||||
|
toast.loading("Encerrando ticket...", { id: "close-ticket" })
|
||||||
|
try {
|
||||||
|
await updateStatus({ ticketId: ticketId as unknown as Id<"tickets">, status: "RESOLVED", actorId })
|
||||||
|
const withPlaceholders = applyTemplatePlaceholders(message, requesterName)
|
||||||
|
const sanitized = sanitizeEditorHtml(withPlaceholders)
|
||||||
|
const hasContent = sanitized.replace(/<[^>]*>/g, "").trim().length > 0
|
||||||
|
if (hasContent) {
|
||||||
|
await addComment({
|
||||||
|
ticketId: ticketId as unknown as Id<"tickets">,
|
||||||
|
authorId: actorId,
|
||||||
|
visibility: "PUBLIC",
|
||||||
|
body: sanitized,
|
||||||
|
attachments: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
toast.success("Ticket encerrado com sucesso!", { id: "close-ticket" })
|
||||||
|
onOpenChange(false)
|
||||||
|
onSuccess()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
toast.error("Não foi possível encerrar o ticket.", { id: "close-ticket" })
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Encerrar ticket</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Confirme a mensagem de encerramento que será enviada ao cliente. Você pode personalizar o texto antes de concluir.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium text-neutral-800">Modelos rápidos</p>
|
||||||
|
{templatesLoading ? (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-neutral-600">
|
||||||
|
<Spinner className="size-4" /> Carregando templates...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{templates.map((template) => (
|
||||||
|
<Button
|
||||||
|
key={template.id}
|
||||||
|
type="button"
|
||||||
|
variant={selectedTemplateId === template.id ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleTemplateSelect(template)}
|
||||||
|
>
|
||||||
|
{template.title}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-neutral-500">
|
||||||
|
Use <code className="rounded bg-slate-100 px-1 py-0.5">{"{{cliente}}"}</code> dentro do template para inserir automaticamente o nome do solicitante.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm font-medium text-neutral-800">Mensagem de encerramento</p>
|
||||||
|
<RichTextEditor
|
||||||
|
value={message}
|
||||||
|
onChange={setMessage}
|
||||||
|
minHeight={220}
|
||||||
|
placeholder="Escreva uma mensagem final para o cliente..."
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
<DialogFooter className="flex flex-wrap justify-between gap-3 pt-4">
|
||||||
|
<div className="text-xs text-neutral-500">
|
||||||
|
O comentário será público e ficará registrado no histórico do ticket.
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setMessage("")
|
||||||
|
setSelectedTemplateId(null)
|
||||||
|
}}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Limpar mensagem
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={handleSubmit} disabled={isSubmitting || !actorId}>
|
||||||
|
{isSubmitting ? <Spinner className="size-4 text-white" /> : "Encerrar ticket"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -10,10 +10,7 @@ import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||||
import { CheckCircle2 } from "lucide-react"
|
import { CheckCircle2 } from "lucide-react"
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
import { CloseTicketDialog } from "@/components/tickets/close-ticket-dialog"
|
||||||
import { Spinner } from "@/components/ui/spinner"
|
|
||||||
import { RichTextEditor } from "@/components/ui/rich-text-editor"
|
|
||||||
import { toast } from "sonner"
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { sanitizeEditorHtml } from "@/components/ui/rich-text-editor"
|
import { sanitizeEditorHtml } from "@/components/ui/rich-text-editor"
|
||||||
|
|
||||||
|
|
@ -124,6 +121,7 @@ export function StatusSelect({
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Dialog control moved to header; keep for status-only contexts */}
|
||||||
<CloseTicketDialog
|
<CloseTicketDialog
|
||||||
open={closeDialogOpen}
|
open={closeDialogOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
|
|
@ -141,168 +139,3 @@ export function StatusSelect({
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CloseTicketDialogProps = {
|
|
||||||
open: boolean
|
|
||||||
onOpenChange: (open: boolean) => void
|
|
||||||
ticketId: string
|
|
||||||
tenantId: string
|
|
||||||
actorId: Id<"users"> | null
|
|
||||||
requesterName?: string | null
|
|
||||||
onSuccess: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
function CloseTicketDialog({ open, onOpenChange, ticketId, tenantId, actorId, requesterName, onSuccess }: CloseTicketDialogProps) {
|
|
||||||
const updateStatus = useMutation(api.tickets.updateStatus)
|
|
||||||
const addComment = useMutation(api.tickets.addComment)
|
|
||||||
|
|
||||||
const closingTemplates = useQuery(
|
|
||||||
actorId && open ? api.commentTemplates.list : "skip",
|
|
||||||
actorId && open ? { tenantId, viewerId: actorId, kind: "closing" as const } : "skip"
|
|
||||||
) as { id: string; title: string; body: string }[] | undefined
|
|
||||||
|
|
||||||
const templatesLoading = Boolean(actorId && open && closingTemplates === undefined)
|
|
||||||
|
|
||||||
const templates = useMemo<ClosingTemplate[]>(() => {
|
|
||||||
if (closingTemplates && closingTemplates.length > 0) {
|
|
||||||
return closingTemplates.map((template) => ({ id: template.id, title: template.title, body: template.body }))
|
|
||||||
}
|
|
||||||
return DEFAULT_CLOSING_TEMPLATES
|
|
||||||
}, [closingTemplates])
|
|
||||||
|
|
||||||
const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(null)
|
|
||||||
const [message, setMessage] = useState<string>("")
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
setSelectedTemplateId(null)
|
|
||||||
setMessage("")
|
|
||||||
setIsSubmitting(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (templates.length > 0 && !selectedTemplateId && !message) {
|
|
||||||
const first = templates[0]
|
|
||||||
const hydrated = sanitizeEditorHtml(applyTemplatePlaceholders(first.body, requesterName))
|
|
||||||
setSelectedTemplateId(first.id)
|
|
||||||
setMessage(hydrated)
|
|
||||||
}
|
|
||||||
}, [open, templates, requesterName, selectedTemplateId, message])
|
|
||||||
|
|
||||||
const handleTemplateSelect = (template: ClosingTemplate) => {
|
|
||||||
setSelectedTemplateId(template.id)
|
|
||||||
const filled = sanitizeEditorHtml(applyTemplatePlaceholders(template.body, requesterName))
|
|
||||||
setMessage(filled)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!actorId) {
|
|
||||||
toast.error("É necessário estar autenticado para encerrar o ticket.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setIsSubmitting(true)
|
|
||||||
toast.loading("Encerrando ticket...", { id: "close-ticket" })
|
|
||||||
try {
|
|
||||||
await updateStatus({ ticketId: ticketId as unknown as Id<"tickets">, status: "RESOLVED", actorId })
|
|
||||||
const withPlaceholders = applyTemplatePlaceholders(message, requesterName)
|
|
||||||
const sanitized = sanitizeEditorHtml(withPlaceholders)
|
|
||||||
const hasContent = sanitized.replace(/<[^>]*>/g, "").trim().length > 0
|
|
||||||
if (hasContent) {
|
|
||||||
await addComment({
|
|
||||||
ticketId: ticketId as unknown as Id<"tickets">,
|
|
||||||
authorId: actorId,
|
|
||||||
visibility: "PUBLIC",
|
|
||||||
body: sanitized,
|
|
||||||
attachments: [],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
toast.success("Ticket encerrado com sucesso!", { id: "close-ticket" })
|
|
||||||
onOpenChange(false)
|
|
||||||
onSuccess()
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
toast.error("Não foi possível encerrar o ticket.", { id: "close-ticket" })
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent className="max-w-2xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Encerrar ticket</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Confirme a mensagem de encerramento que será enviada ao cliente. Você pode personalizar o texto antes de concluir.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-sm font-medium text-neutral-800">Modelos rápidos</p>
|
|
||||||
{templatesLoading ? (
|
|
||||||
<div className="flex items-center gap-2 text-sm text-neutral-600">
|
|
||||||
<Spinner className="size-4" /> Carregando templates...
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{templates.map((template) => (
|
|
||||||
<Button
|
|
||||||
key={template.id}
|
|
||||||
type="button"
|
|
||||||
variant={selectedTemplateId === template.id ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleTemplateSelect(template)}
|
|
||||||
>
|
|
||||||
{template.title}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="text-xs text-neutral-500">
|
|
||||||
Use <code className="rounded bg-slate-100 px-1 py-0.5">{"{{cliente}}"}</code> dentro do template para inserir automaticamente o nome do solicitante.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-sm font-medium text-neutral-800">Mensagem de encerramento</p>
|
|
||||||
<RichTextEditor
|
|
||||||
value={message}
|
|
||||||
onChange={setMessage}
|
|
||||||
minHeight={220}
|
|
||||||
placeholder="Escreva uma mensagem final para o cliente..."
|
|
||||||
/>
|
|
||||||
<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>
|
|
||||||
<DialogFooter className="flex flex-wrap justify-between gap-3 pt-4">
|
|
||||||
<div className="text-xs text-neutral-500">
|
|
||||||
O comentário será público e ficará registrado no histórico do ticket.
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setMessage("")
|
|
||||||
setSelectedTemplateId(null)
|
|
||||||
}}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Limpar mensagem
|
|
||||||
</Button>
|
|
||||||
<Button type="button" onClick={handleSubmit} disabled={isSubmitting || !actorId}>
|
|
||||||
{isSubmitting ? <Spinner className="size-4 text-white" /> : "Encerrar ticket"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import { Separator } from "@/components/ui/separator"
|
||||||
import { PrioritySelect } from "@/components/tickets/priority-select"
|
import { PrioritySelect } from "@/components/tickets/priority-select"
|
||||||
import { DeleteTicketDialog } from "@/components/tickets/delete-ticket-dialog"
|
import { DeleteTicketDialog } from "@/components/tickets/delete-ticket-dialog"
|
||||||
import { StatusSelect } from "@/components/tickets/status-select"
|
import { StatusSelect } from "@/components/tickets/status-select"
|
||||||
|
import { CloseTicketDialog } from "@/components/tickets/close-ticket-dialog"
|
||||||
|
import { CheckCircle2 } from "lucide-react"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
|
|
@ -154,6 +156,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
||||||
const [pauseNote, setPauseNote] = useState("")
|
const [pauseNote, setPauseNote] = useState("")
|
||||||
const [pausing, setPausing] = useState(false)
|
const [pausing, setPausing] = useState(false)
|
||||||
const [exportingPdf, setExportingPdf] = useState(false)
|
const [exportingPdf, setExportingPdf] = useState(false)
|
||||||
|
const [closeOpen, setCloseOpen] = useState(false)
|
||||||
const selectedCategoryId = categorySelection.categoryId
|
const selectedCategoryId = categorySelection.categoryId
|
||||||
const selectedSubcategoryId = categorySelection.subcategoryId
|
const selectedSubcategoryId = categorySelection.subcategoryId
|
||||||
const dirty = useMemo(
|
const dirty = useMemo(
|
||||||
|
|
@ -616,6 +619,13 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div className={cardClass}>
|
<div className={cardClass}>
|
||||||
<div className="absolute right-6 top-6 flex items-center gap-3">
|
<div className="absolute right-6 top-6 flex items-center gap-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center gap-2 rounded-lg border border-sky-300 bg-white px-3 py-1.5 text-sm font-semibold text-sky-700 transition hover:bg-sky-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-200"
|
||||||
|
onClick={() => setCloseOpen(true)}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="size-4" /> Encerrar
|
||||||
|
</Button>
|
||||||
{workSummary ? (
|
{workSummary ? (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|
@ -623,7 +633,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
||||||
className="inline-flex h-9 cursor-help items-center gap-2 rounded-full border border-slate-200 bg-white px-3 text-sm font-semibold text-neutral-700"
|
className="inline-flex h-9 cursor-help items-center gap-2 rounded-full border border-slate-200 bg-white px-3 text-sm font-semibold text-neutral-700"
|
||||||
title="Tempo total de atendimento"
|
title="Tempo total de atendimento"
|
||||||
>
|
>
|
||||||
<IconClock className="size-4 text-neutral-700" /> Tempo total: {formattedTotalWorked}
|
<IconClock className="size-4 text-neutral-700" /> {formattedTotalWorked}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs font-medium text-neutral-700 shadow-lg">
|
<TooltipContent className="rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs font-medium text-neutral-700 shadow-lg">
|
||||||
|
|
@ -658,6 +668,15 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
||||||
</Button>
|
</Button>
|
||||||
<DeleteTicketDialog ticketId={ticket.id as Id<"tickets">} />
|
<DeleteTicketDialog ticketId={ticket.id as Id<"tickets">} />
|
||||||
</div>
|
</div>
|
||||||
|
<CloseTicketDialog
|
||||||
|
open={closeOpen}
|
||||||
|
onOpenChange={setCloseOpen}
|
||||||
|
ticketId={ticket.id as unknown as string}
|
||||||
|
tenantId={ticket.tenantId}
|
||||||
|
actorId={convexUserId as Id<"users"> | null}
|
||||||
|
requesterName={ticket.requester?.name ?? ticket.requester?.email ?? null}
|
||||||
|
onSuccess={() => {}}
|
||||||
|
/>
|
||||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue