sistema-de-chamados/convex/checklistTemplates.ts
rever-tecnologia db73e87cdc debug(checklist): adiciona logs para investigar templateDescription
Adiciona:
- Query debugTemplateAndTicketChecklist para verificar dados no backend
- Console.log no frontend para verificar dados recebidos

Esses logs serao removidos apos identificar o problema.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 16:19:37 -03:00

376 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()
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 }
},
})