feat: checklists em tickets + automações

- Adiciona checklist no ticket (itens obrigatórios/opcionais) e bloqueia encerramento com pendências\n- Cria templates de checklist (globais/por empresa) + tela em /settings/checklists\n- Nova ação de automação: aplicar template de checklist\n- Corrige crash do Select (value vazio), warnings de Dialog e dimensionamento de charts\n- Ajusta SMTP (STARTTLS) e melhora teste de integração
This commit is contained in:
esdrasrenan 2025-12-13 20:51:47 -03:00
parent 4306b0504d
commit 88a9ef454e
27 changed files with 2685 additions and 226 deletions

View file

@ -16,6 +16,7 @@ import { getTemplateByKey, normalizeFormTemplateKey } from "./ticketFormTemplate
import { TICKET_FORM_CONFIG } from "./ticketForms.config"
import { renderAutomationEmailHtml, type AutomationEmailProps } from "./reactEmail"
import { buildBaseUrl } from "./url"
import { applyChecklistTemplateToItems, type TicketChecklistItem } from "./ticketChecklist"
type AutomationEmailTarget = "AUTO" | "PORTAL" | "STAFF"
@ -32,6 +33,7 @@ type AutomationAction =
| { type: "SET_FORM_TEMPLATE"; formTemplate: string | null }
| { type: "SET_CHAT_ENABLED"; enabled: boolean }
| { type: "ADD_INTERNAL_COMMENT"; body: string }
| { type: "APPLY_CHECKLIST_TEMPLATE"; templateId: Id<"ticketChecklistTemplates"> }
| {
type: "SEND_EMAIL"
recipients: AutomationEmailRecipient[]
@ -141,6 +143,12 @@ function parseAction(value: unknown): AutomationAction | null {
return { type: "ADD_INTERNAL_COMMENT", body }
}
if (type === "APPLY_CHECKLIST_TEMPLATE") {
const templateId = typeof record.templateId === "string" ? record.templateId : ""
if (!templateId) return null
return { type: "APPLY_CHECKLIST_TEMPLATE", templateId: templateId as Id<"ticketChecklistTemplates"> }
}
if (type === "SEND_EMAIL") {
const subject = typeof record.subject === "string" ? record.subject.trim() : ""
const message = typeof record.message === "string" ? record.message : ""
@ -672,6 +680,10 @@ async function applyActions(
ctaLabel: string
}> = []
let checklist = (Array.isArray((ticket as unknown as { checklist?: unknown }).checklist)
? ((ticket as unknown as { checklist?: unknown }).checklist as TicketChecklistItem[])
: []) as TicketChecklistItem[]
for (const action of actions) {
if (action.type === "SET_PRIORITY") {
const next = action.priority.trim().toUpperCase()
@ -814,6 +826,32 @@ async function applyActions(
continue
}
if (action.type === "APPLY_CHECKLIST_TEMPLATE") {
const template = (await ctx.db.get(action.templateId)) as Doc<"ticketChecklistTemplates"> | null
if (!template || template.tenantId !== ticket.tenantId || template.isArchived === true) {
throw new ConvexError("Template de checklist inválido na automação")
}
if (template.companyId && (!ticket.companyId || String(template.companyId) !== String(ticket.companyId))) {
throw new ConvexError("Template de checklist não pertence à empresa do ticket")
}
const result = applyChecklistTemplateToItems(checklist, template, {
now,
actorId: automation.createdBy ?? undefined,
})
if (result.added > 0) {
checklist = result.checklist
patch.checklist = checklist.length > 0 ? checklist : undefined
}
applied.push({
type: action.type,
details: { templateId: String(template._id), templateName: template.name, added: result.added },
})
continue
}
if (action.type === "SEND_EMAIL") {
const subject = action.subject.trim()
const message = action.message.replace(/\r\n/g, "\n").trim()