feat: improve reports filters and ticket flows
This commit is contained in:
parent
9c74e10675
commit
15d11b6b12
29 changed files with 437 additions and 140 deletions
|
|
@ -593,6 +593,14 @@ export function CloseTicketDialog({
|
|||
}
|
||||
}
|
||||
|
||||
const withPlaceholders = applyTemplatePlaceholders(message, requesterName, agentName)
|
||||
const sanitizedMessage = stripLeadingEmptyParagraphs(sanitizeEditorHtml(withPlaceholders))
|
||||
const hasContent = sanitizedMessage.replace(/<[^>]*>/g, "").trim().length > 0
|
||||
if (!hasContent) {
|
||||
toast.error("Inclua uma mensagem de encerramento antes de finalizar o ticket.", { id: "close-ticket" })
|
||||
return
|
||||
}
|
||||
|
||||
toast.dismiss("close-ticket")
|
||||
setIsSubmitting(true)
|
||||
toast.loading(applyAdjustment ? "Ajustando tempo e encerrando ticket..." : "Encerrando ticket...", { id: "close-ticket" })
|
||||
|
|
@ -630,18 +638,13 @@ export function CloseTicketDialog({
|
|||
resolvedWithTicketId: linkedTicketCandidate ? (linkedTicketCandidate.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
|
||||
if (hasContent) {
|
||||
await addComment({
|
||||
ticketId: ticketId as unknown as Id<"tickets">,
|
||||
authorId: actorId,
|
||||
visibility: "PUBLIC",
|
||||
body: sanitized,
|
||||
attachments: [],
|
||||
})
|
||||
}
|
||||
await addComment({
|
||||
ticketId: ticketId as unknown as Id<"tickets">,
|
||||
authorId: actorId,
|
||||
visibility: "PUBLIC",
|
||||
body: sanitizedMessage,
|
||||
attachments: [],
|
||||
})
|
||||
toast.success("Ticket encerrado com sucesso!", { id: "close-ticket" })
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.removeItem(draftStorageKey)
|
||||
|
|
|
|||
|
|
@ -116,7 +116,6 @@ function TicketRow({ ticket }: { ticket: Ticket }) {
|
|||
</span>
|
||||
<span className="line-clamp-1 pr-32 text-base font-semibold text-neutral-900">{ticket.subject}</span>
|
||||
</div>
|
||||
<p className="line-clamp-2 pr-32 text-sm text-neutral-600">{ticket.summary ?? "Sem descrição"}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm text-neutral-600">
|
||||
{categoryBadges.length > 0 ? (
|
||||
|
|
|
|||
|
|
@ -104,7 +104,6 @@ const NO_COMPANY_VALUE = "__no_company__"
|
|||
|
||||
const schema = z.object({
|
||||
subject: z.string().default(""),
|
||||
summary: z.string().optional(),
|
||||
description: z.string().default(""),
|
||||
priority: z.enum(["LOW", "MEDIUM", "HIGH", "URGENT"]).default("MEDIUM"),
|
||||
channel: z.enum(["EMAIL", "WHATSAPP", "CHAT", "PHONE", "API", "MANUAL"]).default("MANUAL"),
|
||||
|
|
@ -131,7 +130,6 @@ export function NewTicketDialog({
|
|||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
subject: "",
|
||||
summary: "",
|
||||
description: "",
|
||||
priority: "MEDIUM",
|
||||
channel: "MANUAL",
|
||||
|
|
@ -551,7 +549,6 @@ export function NewTicketDialog({
|
|||
actorId: convexUserId as Id<"users">,
|
||||
tenantId: DEFAULT_TENANT_ID,
|
||||
subject: subjectTrimmed,
|
||||
summary: values.summary?.trim() || undefined,
|
||||
priority: values.priority,
|
||||
channel: values.channel,
|
||||
queueId: sel?.id as Id<"queues"> | undefined,
|
||||
|
|
@ -563,8 +560,8 @@ export function NewTicketDialog({
|
|||
customFields: customFieldsPayload.length > 0 ? customFieldsPayload : undefined,
|
||||
visitDate: visitDateTimestamp,
|
||||
})
|
||||
const summaryFallback = values.summary?.trim() ?? ""
|
||||
const bodyHtml = plainDescription.length > 0 ? sanitizedDescription : summaryFallback
|
||||
const summaryFallback = subjectTrimmed
|
||||
const bodyHtml = plainDescription.length > 0 ? sanitizedDescription : toHtml(summaryFallback)
|
||||
const MAX_COMMENT_CHARS = 20000
|
||||
const plainForLimit = (plainDescription.length > 0 ? plainDescription : summaryFallback).trim()
|
||||
if (plainForLimit.length > MAX_COMMENT_CHARS) {
|
||||
|
|
@ -585,7 +582,6 @@ export function NewTicketDialog({
|
|||
setOpen(false)
|
||||
form.reset({
|
||||
subject: "",
|
||||
summary: "",
|
||||
description: "",
|
||||
priority: "MEDIUM",
|
||||
channel: "MANUAL",
|
||||
|
|
@ -704,21 +700,6 @@ export function NewTicketDialog({
|
|||
<Input id="subject" {...form.register("subject")} placeholder="Ex.: Erro 500 no portal" />
|
||||
<FieldError errors={form.formState.errors.subject ? [{ message: form.formState.errors.subject.message }] : []} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="summary">Resumo</FieldLabel>
|
||||
<textarea
|
||||
id="summary"
|
||||
className="min-h-[96px] w-full resize-none overflow-hidden rounded-lg border border-slate-300 bg-background p-3 text-sm shadow-sm focus-visible:border-[#00d6eb] focus-visible:outline-none"
|
||||
maxLength={600}
|
||||
{...form.register("summary")}
|
||||
placeholder="Explique em poucas linhas o contexto do chamado."
|
||||
onInput={(e) => {
|
||||
const el = e.currentTarget
|
||||
el.style.height = 'auto'
|
||||
el.style.height = `${el.scrollHeight}px`
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel className="flex items-center gap-1">
|
||||
Descrição <span className="text-destructive">*</span>
|
||||
|
|
|
|||
|
|
@ -117,7 +117,6 @@ export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) {
|
|||
</div>
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-xl font-semibold text-neutral-900">{ticket.subject}</h2>
|
||||
<p className="text-sm text-neutral-600">{ticket.summary}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-neutral-600">
|
||||
<Badge className={queueBadgeClass}>{ticket.queue ?? "Sem fila"}</Badge>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ function TicketRow({ ticket, entering }: { ticket: Ticket; entering: boolean })
|
|||
<span className="line-clamp-1 text-[20px] font-semibold text-neutral-900 transition-colors group-hover:text-neutral-700">
|
||||
{ticket.subject}
|
||||
</span>
|
||||
<p className="line-clamp-2 text-base text-neutral-600">{ticket.summary ?? "Sem descrição informada."}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm text-neutral-600">
|
||||
<span className="font-semibold text-neutral-700">{requesterName}</span>
|
||||
|
|
|
|||
|
|
@ -193,7 +193,6 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
const changeQueue = useMutation(api.tickets.changeQueue)
|
||||
const changeRequester = useMutation(api.tickets.changeRequester)
|
||||
const updateSubject = useMutation(api.tickets.updateSubject)
|
||||
const updateSummary = useMutation(api.tickets.updateSummary)
|
||||
const startWork = useMutation(api.tickets.startWork)
|
||||
const pauseWork = useMutation(api.tickets.pauseWork)
|
||||
const updateCategories = useMutation(api.tickets.updateCategories)
|
||||
|
|
@ -263,7 +262,6 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
const [assigneeState, setAssigneeState] = useState(ticket.assignee ?? null)
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [subject, setSubject] = useState(ticket.subject)
|
||||
const [summary, setSummary] = useState(ticket.summary ?? "")
|
||||
const [categorySelection, setCategorySelection] = useState<{ categoryId: string; subcategoryId: string }>(
|
||||
{
|
||||
categoryId: ticket.category?.id ?? "",
|
||||
|
|
@ -288,10 +286,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
const [customersInitialized, setCustomersInitialized] = useState(false)
|
||||
const selectedCategoryId = categorySelection.categoryId
|
||||
const selectedSubcategoryId = categorySelection.subcategoryId
|
||||
const dirty = useMemo(
|
||||
() => subject !== ticket.subject || (summary ?? "") !== (ticket.summary ?? ""),
|
||||
[subject, summary, ticket.subject, ticket.summary]
|
||||
)
|
||||
const dirty = useMemo(() => subject !== ticket.subject, [subject, ticket.subject])
|
||||
const currentCategoryId = ticket.category?.id ?? ""
|
||||
const currentSubcategoryId = ticket.subcategory?.id ?? ""
|
||||
const categoryDirty = useMemo(() => {
|
||||
|
|
@ -616,13 +611,6 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
actorId: convexUserId as Id<"users">,
|
||||
})
|
||||
}
|
||||
if ((summary ?? "") !== (ticket.summary ?? "")) {
|
||||
await updateSummary({
|
||||
ticketId: ticket.id as Id<"tickets">,
|
||||
summary: (summary ?? "").trim(),
|
||||
actorId: convexUserId as Id<"users">,
|
||||
})
|
||||
}
|
||||
toast.success("Cabeçalho atualizado!", { id: "save-header" })
|
||||
}
|
||||
setEditing(false)
|
||||
|
|
@ -635,7 +623,6 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
|
||||
function handleCancel() {
|
||||
setSubject(ticket.subject)
|
||||
setSummary(ticket.summary ?? "")
|
||||
setCategorySelection({
|
||||
categoryId: currentCategoryId,
|
||||
subcategoryId: currentSubcategoryId,
|
||||
|
|
@ -1299,25 +1286,6 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
onChange={(e) => setSubject(e.target.value)}
|
||||
className="h-10 rounded-lg border border-slate-300 text-lg font-semibold text-neutral-900"
|
||||
/>
|
||||
<textarea
|
||||
value={summary}
|
||||
onChange={(e) => {
|
||||
const el = e.currentTarget
|
||||
// auto-resize height based on content
|
||||
el.style.height = 'auto'
|
||||
el.style.height = `${el.scrollHeight}px`
|
||||
setSummary(e.target.value)
|
||||
}}
|
||||
onInput={(e) => {
|
||||
const el = e.currentTarget
|
||||
el.style.height = 'auto'
|
||||
el.style.height = `${el.scrollHeight}px`
|
||||
}}
|
||||
rows={3}
|
||||
maxLength={600}
|
||||
className="w-full resize-none overflow-hidden rounded-lg border border-slate-300 bg-white p-3 text-sm text-neutral-800 shadow-sm"
|
||||
placeholder="Adicione um resumo opcional"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
|
|
@ -1329,7 +1297,6 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{summary ? <p className="max-w-2xl text-sm text-neutral-600">{summary}</p> : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -218,9 +218,6 @@ export function TicketsTable({ tickets, enteringIds }: TicketsTableProps) {
|
|||
<span className="text-[15px] font-semibold text-neutral-900 line-clamp-2 md:line-clamp-1 break-words">
|
||||
{ticket.subject}
|
||||
</span>
|
||||
<span className="text-sm text-neutral-600 line-clamp-1 break-words">
|
||||
{ticket.summary ?? "Sem resumo"}
|
||||
</span>
|
||||
{ticket.formTemplateLabel || ticket.formTemplate ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className="rounded-full border border-sky-200 bg-sky-50 px-2.5 py-0.5 text-[11px] font-semibold text-sky-700">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue