import { mutation, query } from "./_generated/server" import { ConvexError, v } from "convex/values" import { requireAdmin } from "./rbac" const PRIORITY_VALUES = ["URGENT", "HIGH", "MEDIUM", "LOW", "DEFAULT"] as const const VALID_STATUSES = ["PENDING", "AWAITING_ATTENDANCE", "PAUSED", "RESOLVED"] as const const VALID_TIME_MODES = ["business", "calendar"] as const type CompanySlaRuleInput = { priority: string categoryId?: string | null responseTargetMinutes?: number | null responseMode?: string | null solutionTargetMinutes?: number | null solutionMode?: string | null alertThreshold?: number | null pauseStatuses?: string[] | null calendarType?: string | null } const ruleInput = v.object({ priority: v.string(), categoryId: v.optional(v.union(v.id("ticketCategories"), v.null())), responseTargetMinutes: v.optional(v.number()), responseMode: v.optional(v.string()), solutionTargetMinutes: v.optional(v.number()), solutionMode: v.optional(v.string()), alertThreshold: v.optional(v.number()), pauseStatuses: v.optional(v.array(v.string())), calendarType: v.optional(v.string()), }) function normalizePriority(value: string) { const upper = value.trim().toUpperCase() return PRIORITY_VALUES.includes(upper as (typeof PRIORITY_VALUES)[number]) ? upper : "DEFAULT" } function sanitizeTime(value?: number | null) { if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return undefined return Math.round(value) } function normalizeMode(value?: string | null) { if (!value) return "calendar" const normalized = value.toLowerCase() return VALID_TIME_MODES.includes(normalized as (typeof VALID_TIME_MODES)[number]) ? normalized : "calendar" } function normalizeThreshold(value?: number | null) { if (typeof value !== "number" || Number.isNaN(value)) { return 0.8 } const clamped = Math.min(Math.max(value, 0.1), 0.95) return Math.round(clamped * 100) / 100 } function normalizePauseStatuses(value?: string[] | null) { if (!Array.isArray(value)) return ["PAUSED"] const normalized = new Set() for (const status of value) { if (typeof status !== "string") continue const upper = status.trim().toUpperCase() if (VALID_STATUSES.includes(upper as (typeof VALID_STATUSES)[number])) { normalized.add(upper) } } if (normalized.size === 0) { normalized.add("PAUSED") } return Array.from(normalized) } // Lista todas as empresas que possuem SLA customizado export const listCompaniesWithCustomSla = query({ args: { tenantId: v.string(), viewerId: v.id("users"), }, handler: async (ctx, { tenantId, viewerId }) => { await requireAdmin(ctx, viewerId, tenantId) // Busca todas as configurações de SLA por empresa const allSettings = await ctx.db .query("companySlaSettings") .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId)) .take(1000) // Agrupa por companyId para evitar duplicatas const companyIds = [...new Set(allSettings.map((s) => s.companyId))] // Busca dados das empresas const companies = await Promise.all( companyIds.map(async (companyId) => { const company = await ctx.db.get(companyId) if (!company) return null const rulesCount = allSettings.filter((s) => s.companyId === companyId).length return { companyId, companyName: company.name, companySlug: company.slug, rulesCount, } }) ) return companies.filter(Boolean) }, }) // Busca as regras de SLA de uma empresa específica export const get = query({ args: { tenantId: v.string(), viewerId: v.id("users"), companyId: v.id("companies"), }, handler: async (ctx, { tenantId, viewerId, companyId }) => { await requireAdmin(ctx, viewerId, tenantId) const company = await ctx.db.get(companyId) if (!company || company.tenantId !== tenantId) { throw new ConvexError("Empresa não encontrada") } const records = await ctx.db .query("companySlaSettings") .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId)) .take(100) // Busca nomes das categorias referenciadas const categoryIds = [...new Set(records.filter((r) => r.categoryId).map((r) => r.categoryId!))] const categories = await Promise.all(categoryIds.map((id) => ctx.db.get(id))) const categoryNames = new Map( categories.filter(Boolean).map((c) => [c!._id, c!.name]) ) return { companyId, companyName: company.name, rules: records.map((record) => ({ priority: record.priority, categoryId: record.categoryId ?? null, categoryName: record.categoryId ? categoryNames.get(record.categoryId) ?? null : null, responseTargetMinutes: record.responseTargetMinutes ?? null, responseMode: record.responseMode ?? "calendar", solutionTargetMinutes: record.solutionTargetMinutes ?? null, solutionMode: record.solutionMode ?? "calendar", alertThreshold: record.alertThreshold ?? 0.8, pauseStatuses: record.pauseStatuses ?? ["PAUSED"], })), } }, }) // Salva as regras de SLA de uma empresa export const save = mutation({ args: { tenantId: v.string(), actorId: v.id("users"), companyId: v.id("companies"), rules: v.array(ruleInput), }, handler: async (ctx, { tenantId, actorId, companyId, rules }) => { await requireAdmin(ctx, actorId, tenantId) const company = await ctx.db.get(companyId) if (!company || company.tenantId !== tenantId) { throw new ConvexError("Empresa não encontrada") } // Valida categorias referenciadas for (const rule of rules) { if (rule.categoryId) { const category = await ctx.db.get(rule.categoryId) if (!category || category.tenantId !== tenantId) { throw new ConvexError(`Categoria inválida: ${rule.categoryId}`) } } } const sanitized = sanitizeRules(rules) // Remove regras existentes da empresa const existing = await ctx.db .query("companySlaSettings") .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId)) .take(100) await Promise.all(existing.map((record) => ctx.db.delete(record._id))) // Insere novas regras const now = Date.now() for (const rule of sanitized) { await ctx.db.insert("companySlaSettings", { tenantId, companyId, categoryId: rule.categoryId ? (rule.categoryId as Id<"ticketCategories">) : undefined, priority: rule.priority, responseTargetMinutes: rule.responseTargetMinutes, responseMode: rule.responseMode, solutionTargetMinutes: rule.solutionTargetMinutes, solutionMode: rule.solutionMode, alertThreshold: rule.alertThreshold, pauseStatuses: rule.pauseStatuses, calendarType: rule.calendarType ?? undefined, createdAt: now, updatedAt: now, actorId, }) } return { ok: true } }, }) // Remove todas as regras de SLA de uma empresa export const remove = mutation({ args: { tenantId: v.string(), actorId: v.id("users"), companyId: v.id("companies"), }, handler: async (ctx, { tenantId, actorId, companyId }) => { await requireAdmin(ctx, actorId, tenantId) const company = await ctx.db.get(companyId) if (!company || company.tenantId !== tenantId) { throw new ConvexError("Empresa não encontrada") } const existing = await ctx.db .query("companySlaSettings") .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId)) .take(100) await Promise.all(existing.map((record) => ctx.db.delete(record._id))) return { ok: true } }, }) function sanitizeRules(rules: CompanySlaRuleInput[]) { // Chave única: categoryId + priority const normalized: Map> = new Map() for (const rule of rules) { const built = buildRule(rule) const key = `${built.categoryId ?? "ALL"}-${built.priority}` normalized.set(key, built) } return Array.from(normalized.values()) } function buildRule(rule: CompanySlaRuleInput) { const priority = normalizePriority(rule.priority) const responseTargetMinutes = sanitizeTime(rule.responseTargetMinutes) const solutionTargetMinutes = sanitizeTime(rule.solutionTargetMinutes) return { priority, categoryId: rule.categoryId ?? null, responseTargetMinutes, responseMode: normalizeMode(rule.responseMode), solutionTargetMinutes, solutionMode: normalizeMode(rule.solutionMode), alertThreshold: normalizeThreshold(rule.alertThreshold), pauseStatuses: normalizePauseStatuses(rule.pauseStatuses), calendarType: rule.calendarType ?? null, } }