import { mutation, query } from "./_generated/server"; import type { MutationCtx, QueryCtx } from "./_generated/server"; import { ConvexError, v } from "convex/values"; import type { Id } from "./_generated/dataModel"; import { requireAdmin } from "./rbac"; function normalizeName(value: string) { return value.trim(); } type AnyCtx = QueryCtx | MutationCtx; async function ensureUniqueName(ctx: AnyCtx, tenantId: string, name: string, excludeId?: Id<"slaPolicies">) { const existing = await ctx.db .query("slaPolicies") .withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId).eq("name", name)) .first(); if (existing && (!excludeId || existing._id !== excludeId)) { throw new ConvexError("Já existe uma política SLA com este nome"); } } export const list = query({ args: { tenantId: v.string(), viewerId: v.id("users") }, handler: async (ctx, { tenantId, viewerId }) => { await requireAdmin(ctx, viewerId, tenantId); const items = await ctx.db .query("slaPolicies") .withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId)) .take(50); return items.map((policy) => ({ id: policy._id, name: policy.name, description: policy.description ?? "", timeToFirstResponse: policy.timeToFirstResponse ?? null, timeToResolution: policy.timeToResolution ?? null, })); }, }); export const create = mutation({ args: { tenantId: v.string(), actorId: v.id("users"), name: v.string(), description: v.optional(v.string()), timeToFirstResponse: v.optional(v.number()), timeToResolution: v.optional(v.number()), }, handler: async (ctx, { tenantId, actorId, name, description, timeToFirstResponse, timeToResolution }) => { await requireAdmin(ctx, actorId, tenantId); const trimmed = normalizeName(name); if (trimmed.length < 2) { throw new ConvexError("Informe um nome válido para a política"); } await ensureUniqueName(ctx, tenantId, trimmed); if (timeToFirstResponse !== undefined && timeToFirstResponse < 0) { throw new ConvexError("Tempo para primeira resposta deve ser positivo"); } if (timeToResolution !== undefined && timeToResolution < 0) { throw new ConvexError("Tempo para resolução deve ser positivo"); } const id = await ctx.db.insert("slaPolicies", { tenantId, name: trimmed, description, timeToFirstResponse, timeToResolution, }); return id; }, }); export const update = mutation({ args: { policyId: v.id("slaPolicies"), tenantId: v.string(), actorId: v.id("users"), name: v.string(), description: v.optional(v.string()), timeToFirstResponse: v.optional(v.number()), timeToResolution: v.optional(v.number()), }, handler: async (ctx, { policyId, tenantId, actorId, name, description, timeToFirstResponse, timeToResolution }) => { await requireAdmin(ctx, actorId, tenantId); const policy = await ctx.db.get(policyId); if (!policy || policy.tenantId !== tenantId) { throw new ConvexError("Política não encontrada"); } const trimmed = normalizeName(name); if (trimmed.length < 2) { throw new ConvexError("Informe um nome válido para a política"); } if (timeToFirstResponse !== undefined && timeToFirstResponse < 0) { throw new ConvexError("Tempo para primeira resposta deve ser positivo"); } if (timeToResolution !== undefined && timeToResolution < 0) { throw new ConvexError("Tempo para resolução deve ser positivo"); } await ensureUniqueName(ctx, tenantId, trimmed, policyId); await ctx.db.patch(policyId, { name: trimmed, description, timeToFirstResponse, timeToResolution, }); }, }); export const remove = mutation({ args: { policyId: v.id("slaPolicies"), tenantId: v.string(), actorId: v.id("users"), }, handler: async (ctx, { policyId, tenantId, actorId }) => { await requireAdmin(ctx, actorId, tenantId); const policy = await ctx.db.get(policyId); if (!policy || policy.tenantId !== tenantId) { throw new ConvexError("Política não encontrada"); } const ticketLinked = await ctx.db .query("tickets") .withIndex("by_tenant_sla_policy", (q) => q.eq("tenantId", tenantId).eq("slaPolicyId", policyId)) .first(); if (ticketLinked) { throw new ConvexError("Remova a associação de tickets antes de excluir a política"); } await ctx.db.delete(policyId); }, });