sistema-de-chamados/convex/checklistTemplates.ts
esdrasrenan 51c9cab79c fix: criação de template de checklist
- Permite isArchived na mutation checklistTemplates.create\n- Troca Select por SearchableCombobox no campo Empresa
2025-12-13 21:14:51 -03:00

261 lines
8.2 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
}
function normalizeTemplateItems(
raw: Array<{ id?: string; text: string; required?: boolean }>,
options: { generateId?: () => string }
) {
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: Array<{ id: string; text: string; required?: boolean }> = []
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 required = typeof entry.required === "boolean" ? entry.required : true
items.push({ id, text, 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,
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(),
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(),
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 }
},
})