Logs adicionados: - Na criacao do template para ver se description esta sendo salva - Na aplicacao do template para ver se description existe 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
384 lines
12 KiB
TypeScript
384 lines
12 KiB
TypeScript
import { ConvexError, v } from "convex/values"
|
|
|
|
import type { Doc, Id } from "./_generated/dataModel"
|
|
import { mutation, query } from "./_generated/server"
|
|
import { requireAdmin, requireStaff } from "./rbac"
|
|
import { normalizeChecklistText } from "./ticketChecklist"
|
|
|
|
function normalizeTemplateName(input: string) {
|
|
return input.trim()
|
|
}
|
|
|
|
function normalizeTemplateDescription(input: string | null | undefined) {
|
|
const text = (input ?? "").trim()
|
|
return text.length > 0 ? text : null
|
|
}
|
|
|
|
type ChecklistItemType = "checkbox" | "question"
|
|
|
|
type RawTemplateItem = {
|
|
id?: string
|
|
text: string
|
|
description?: string
|
|
type?: string
|
|
options?: string[]
|
|
required?: boolean
|
|
}
|
|
|
|
type NormalizedTemplateItem = {
|
|
id: string
|
|
text: string
|
|
description?: string
|
|
type?: ChecklistItemType
|
|
options?: string[]
|
|
required?: boolean
|
|
}
|
|
|
|
function normalizeTemplateItems(
|
|
raw: RawTemplateItem[],
|
|
options: { generateId?: () => string }
|
|
): NormalizedTemplateItem[] {
|
|
if (!Array.isArray(raw) || raw.length === 0) {
|
|
throw new ConvexError("Adicione pelo menos um item no checklist.")
|
|
}
|
|
|
|
const generateId = options.generateId ?? (() => crypto.randomUUID())
|
|
const seen = new Set<string>()
|
|
const items: NormalizedTemplateItem[] = []
|
|
|
|
for (const entry of raw) {
|
|
const id = String(entry.id ?? "").trim() || generateId()
|
|
if (seen.has(id)) {
|
|
throw new ConvexError("Itens do checklist com IDs duplicados.")
|
|
}
|
|
seen.add(id)
|
|
|
|
const text = normalizeChecklistText(entry.text)
|
|
if (!text) {
|
|
throw new ConvexError("Todos os itens do checklist precisam ter um texto.")
|
|
}
|
|
if (text.length > 240) {
|
|
throw new ConvexError("Item do checklist muito longo (máx. 240 caracteres).")
|
|
}
|
|
|
|
const description = entry.description?.trim() || undefined
|
|
const itemType: ChecklistItemType = entry.type === "question" ? "question" : "checkbox"
|
|
const itemOptions = itemType === "question" && Array.isArray(entry.options)
|
|
? entry.options.map((o) => String(o).trim()).filter((o) => o.length > 0)
|
|
: undefined
|
|
|
|
if (itemType === "question" && (!itemOptions || itemOptions.length < 2)) {
|
|
throw new ConvexError(`A pergunta "${text}" precisa ter pelo menos 2 opções.`)
|
|
}
|
|
|
|
const required = typeof entry.required === "boolean" ? entry.required : true
|
|
items.push({
|
|
id,
|
|
text,
|
|
description,
|
|
type: itemType,
|
|
options: itemOptions,
|
|
required,
|
|
})
|
|
}
|
|
|
|
return items
|
|
}
|
|
|
|
function mapTemplate(template: Doc<"ticketChecklistTemplates">, company: Doc<"companies"> | null) {
|
|
return {
|
|
id: template._id,
|
|
name: template.name,
|
|
description: template.description ?? "",
|
|
company: company ? { id: company._id, name: company.name } : null,
|
|
items: (template.items ?? []).map((item) => ({
|
|
id: item.id,
|
|
text: item.text,
|
|
description: item.description,
|
|
type: item.type ?? "checkbox",
|
|
options: item.options,
|
|
required: typeof item.required === "boolean" ? item.required : true,
|
|
})),
|
|
isArchived: Boolean(template.isArchived),
|
|
createdAt: template.createdAt,
|
|
updatedAt: template.updatedAt,
|
|
}
|
|
}
|
|
|
|
export const listActive = query({
|
|
args: {
|
|
tenantId: v.string(),
|
|
viewerId: v.id("users"),
|
|
companyId: v.optional(v.id("companies")),
|
|
},
|
|
handler: async (ctx, { tenantId, viewerId, companyId }) => {
|
|
await requireStaff(ctx, viewerId, tenantId)
|
|
|
|
const templates = await ctx.db
|
|
.query("ticketChecklistTemplates")
|
|
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
|
.take(200)
|
|
|
|
const filtered = templates.filter((tpl) => {
|
|
if (tpl.isArchived === true) return false
|
|
if (!companyId) return true
|
|
return !tpl.companyId || String(tpl.companyId) === String(companyId)
|
|
})
|
|
|
|
const companiesToHydrate = new Map<string, Id<"companies">>()
|
|
for (const tpl of filtered) {
|
|
if (tpl.companyId) {
|
|
companiesToHydrate.set(String(tpl.companyId), tpl.companyId)
|
|
}
|
|
}
|
|
|
|
const companyMap = new Map<string, Doc<"companies">>()
|
|
for (const id of companiesToHydrate.values()) {
|
|
const company = await ctx.db.get(id)
|
|
if (company && company.tenantId === tenantId) {
|
|
companyMap.set(String(id), company as Doc<"companies">)
|
|
}
|
|
}
|
|
|
|
return filtered
|
|
.sort((a, b) => {
|
|
const aSpecific = a.companyId ? 1 : 0
|
|
const bSpecific = b.companyId ? 1 : 0
|
|
if (aSpecific !== bSpecific) return bSpecific - aSpecific
|
|
return (a.name ?? "").localeCompare(b.name ?? "", "pt-BR")
|
|
})
|
|
.map((tpl) => mapTemplate(tpl, tpl.companyId ? (companyMap.get(String(tpl.companyId)) ?? null) : null))
|
|
},
|
|
})
|
|
|
|
export const list = query({
|
|
args: {
|
|
tenantId: v.string(),
|
|
viewerId: v.id("users"),
|
|
includeArchived: v.optional(v.boolean()),
|
|
},
|
|
handler: async (ctx, { tenantId, viewerId, includeArchived }) => {
|
|
await requireAdmin(ctx, viewerId, tenantId)
|
|
|
|
const templates = await ctx.db
|
|
.query("ticketChecklistTemplates")
|
|
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
|
.take(500)
|
|
|
|
const filtered = templates.filter((tpl) => includeArchived || tpl.isArchived !== true)
|
|
|
|
const companiesToHydrate = new Map<string, Id<"companies">>()
|
|
for (const tpl of filtered) {
|
|
if (tpl.companyId) {
|
|
companiesToHydrate.set(String(tpl.companyId), tpl.companyId)
|
|
}
|
|
}
|
|
|
|
const companyMap = new Map<string, Doc<"companies">>()
|
|
for (const id of companiesToHydrate.values()) {
|
|
const company = await ctx.db.get(id)
|
|
if (company && company.tenantId === tenantId) {
|
|
companyMap.set(String(id), company as Doc<"companies">)
|
|
}
|
|
}
|
|
|
|
return filtered
|
|
.sort((a, b) => {
|
|
const aSpecific = a.companyId ? 1 : 0
|
|
const bSpecific = b.companyId ? 1 : 0
|
|
if (aSpecific !== bSpecific) return bSpecific - aSpecific
|
|
return (a.name ?? "").localeCompare(b.name ?? "", "pt-BR")
|
|
})
|
|
.map((tpl) => mapTemplate(tpl, tpl.companyId ? (companyMap.get(String(tpl.companyId)) ?? null) : null))
|
|
},
|
|
})
|
|
|
|
export const create = mutation({
|
|
args: {
|
|
tenantId: v.string(),
|
|
actorId: v.id("users"),
|
|
name: v.string(),
|
|
description: v.optional(v.string()),
|
|
companyId: v.optional(v.id("companies")),
|
|
items: v.array(
|
|
v.object({
|
|
id: v.optional(v.string()),
|
|
text: v.string(),
|
|
description: v.optional(v.string()),
|
|
type: v.optional(v.string()),
|
|
options: v.optional(v.array(v.string())),
|
|
required: v.optional(v.boolean()),
|
|
}),
|
|
),
|
|
isArchived: v.optional(v.boolean()),
|
|
},
|
|
handler: async (ctx, { tenantId, actorId, name, description, companyId, items, isArchived }) => {
|
|
await requireAdmin(ctx, actorId, tenantId)
|
|
|
|
const normalizedName = normalizeTemplateName(name)
|
|
if (normalizedName.length < 3) {
|
|
throw new ConvexError("Informe um nome com pelo menos 3 caracteres.")
|
|
}
|
|
|
|
if (companyId) {
|
|
const company = await ctx.db.get(companyId)
|
|
if (!company || company.tenantId !== tenantId) {
|
|
throw new ConvexError("Empresa inválida para o template.")
|
|
}
|
|
}
|
|
|
|
const normalizedItems = normalizeTemplateItems(items, {})
|
|
const normalizedDescription = normalizeTemplateDescription(description)
|
|
const archivedFlag = typeof isArchived === "boolean" ? isArchived : false
|
|
const now = Date.now()
|
|
|
|
// DEBUG: Verificar descrição sendo salva
|
|
console.log("[DEBUG create template]", {
|
|
name: normalizedName,
|
|
descriptionInput: description,
|
|
normalizedDescription,
|
|
willSave: normalizedDescription ?? undefined,
|
|
})
|
|
|
|
return ctx.db.insert("ticketChecklistTemplates", {
|
|
tenantId,
|
|
name: normalizedName,
|
|
description: normalizedDescription ?? undefined,
|
|
companyId: companyId ?? undefined,
|
|
items: normalizedItems,
|
|
isArchived: archivedFlag,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
createdBy: actorId,
|
|
updatedBy: actorId,
|
|
})
|
|
},
|
|
})
|
|
|
|
export const update = mutation({
|
|
args: {
|
|
tenantId: v.string(),
|
|
actorId: v.id("users"),
|
|
templateId: v.id("ticketChecklistTemplates"),
|
|
name: v.string(),
|
|
description: v.optional(v.string()),
|
|
companyId: v.optional(v.id("companies")),
|
|
items: v.array(
|
|
v.object({
|
|
id: v.optional(v.string()),
|
|
text: v.string(),
|
|
description: v.optional(v.string()),
|
|
type: v.optional(v.string()),
|
|
options: v.optional(v.array(v.string())),
|
|
required: v.optional(v.boolean()),
|
|
}),
|
|
),
|
|
isArchived: v.optional(v.boolean()),
|
|
},
|
|
handler: async (ctx, { tenantId, actorId, templateId, name, description, companyId, items, isArchived }) => {
|
|
await requireAdmin(ctx, actorId, tenantId)
|
|
|
|
const existing = await ctx.db.get(templateId)
|
|
if (!existing || existing.tenantId !== tenantId) {
|
|
throw new ConvexError("Template de checklist não encontrado.")
|
|
}
|
|
|
|
const normalizedName = normalizeTemplateName(name)
|
|
if (normalizedName.length < 3) {
|
|
throw new ConvexError("Informe um nome com pelo menos 3 caracteres.")
|
|
}
|
|
|
|
if (companyId) {
|
|
const company = await ctx.db.get(companyId)
|
|
if (!company || company.tenantId !== tenantId) {
|
|
throw new ConvexError("Empresa inválida para o template.")
|
|
}
|
|
}
|
|
|
|
const normalizedItems = normalizeTemplateItems(items, {})
|
|
const normalizedDescription = normalizeTemplateDescription(description)
|
|
const nextArchived = typeof isArchived === "boolean" ? isArchived : Boolean(existing.isArchived)
|
|
const now = Date.now()
|
|
|
|
await ctx.db.patch(templateId, {
|
|
name: normalizedName,
|
|
description: normalizedDescription ?? undefined,
|
|
companyId: companyId ?? undefined,
|
|
items: normalizedItems,
|
|
isArchived: nextArchived,
|
|
updatedAt: now,
|
|
updatedBy: actorId,
|
|
})
|
|
|
|
return { ok: true }
|
|
},
|
|
})
|
|
|
|
export const remove = mutation({
|
|
args: {
|
|
tenantId: v.string(),
|
|
actorId: v.id("users"),
|
|
templateId: v.id("ticketChecklistTemplates"),
|
|
},
|
|
handler: async (ctx, { tenantId, actorId, templateId }) => {
|
|
await requireAdmin(ctx, actorId, tenantId)
|
|
|
|
const existing = await ctx.db.get(templateId)
|
|
if (!existing || existing.tenantId !== tenantId) {
|
|
throw new ConvexError("Template de checklist não encontrado.")
|
|
}
|
|
|
|
await ctx.db.delete(templateId)
|
|
|
|
return { ok: true }
|
|
},
|
|
})
|
|
|
|
// DEBUG: Query para verificar dados do template e checklist de um ticket
|
|
export const debugTemplateAndTicketChecklist = query({
|
|
args: {
|
|
tenantId: v.string(),
|
|
viewerId: v.id("users"),
|
|
templateId: v.id("ticketChecklistTemplates"),
|
|
ticketId: v.optional(v.id("tickets")),
|
|
},
|
|
handler: async (ctx, { tenantId, viewerId, templateId, ticketId }) => {
|
|
await requireStaff(ctx, viewerId, tenantId)
|
|
|
|
const template = await ctx.db.get(templateId)
|
|
if (!template || template.tenantId !== tenantId) {
|
|
return { error: "Template nao encontrado" }
|
|
}
|
|
|
|
const templateData = {
|
|
id: String(template._id),
|
|
name: template.name,
|
|
description: template.description,
|
|
hasDescription: Boolean(template.description),
|
|
descriptionType: typeof template.description,
|
|
itemsCount: template.items?.length ?? 0,
|
|
}
|
|
|
|
let ticketData = null
|
|
if (ticketId) {
|
|
const ticket = await ctx.db.get(ticketId)
|
|
if (ticket && ticket.tenantId === tenantId) {
|
|
ticketData = {
|
|
id: String(ticket._id),
|
|
checklistCount: ticket.checklist?.length ?? 0,
|
|
checklistItems: (ticket.checklist ?? []).map((item) => ({
|
|
id: item.id,
|
|
text: item.text.substring(0, 50),
|
|
templateId: item.templateId ? String(item.templateId) : null,
|
|
templateDescription: item.templateDescription,
|
|
hasTemplateDescription: Boolean(item.templateDescription),
|
|
description: item.description,
|
|
hasDescription: Boolean(item.description),
|
|
})),
|
|
}
|
|
}
|
|
}
|
|
|
|
return { template: templateData, ticket: ticketData }
|
|
},
|
|
})
|