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, requireStaff } from "./rbac"; const QUEUE_RENAME_LOOKUP: Record = { "Suporte N1": "Chamados", "suporte-n1": "Chamados", chamados: "Chamados", "Suporte N2": "Laboratório", "suporte-n2": "Laboratório", laboratorio: "Laboratório", Laboratorio: "Laboratório", visitas: "Visitas", }; function renameQueueString(value: string) { const direct = QUEUE_RENAME_LOOKUP[value]; if (direct) return direct; const normalizedKey = value.replace(/\s+/g, "-").toLowerCase(); return QUEUE_RENAME_LOOKUP[normalizedKey] ?? value; } function slugify(value: string) { return value .normalize("NFD") .replace(/[^\w\s-]/g, "") .trim() .replace(/\s+/g, "-") .replace(/-+/g, "-") .toLowerCase(); } type AnyCtx = QueryCtx | MutationCtx; async function ensureUniqueSlug(ctx: AnyCtx, tenantId: string, slug: string, excludeId?: Id<"queues">) { const existing = await ctx.db .query("queues") .withIndex("by_tenant_slug", (q) => q.eq("tenantId", tenantId).eq("slug", slug)) .first(); if (existing && (!excludeId || existing._id !== excludeId)) { throw new ConvexError("Já existe uma fila com este identificador"); } } async function ensureUniqueName(ctx: AnyCtx, tenantId: string, name: string, excludeId?: Id<"queues">) { const existing = await ctx.db .query("queues") .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .filter((q) => q.eq(q.field("name"), name)) .first(); if (existing && (!excludeId || existing._id !== excludeId)) { throw new ConvexError("Já existe uma fila 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 queues = await ctx.db .query("queues") .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .collect(); const teams = await ctx.db .query("teams") .withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId)) .collect(); return queues.map((queue) => { const team = queue.teamId ? teams.find((item) => item._id === queue.teamId) : null; return { id: queue._id, name: queue.name, slug: queue.slug, team: team ? { id: team._id, name: team.name, } : null, }; }); }, }); export const summary = query({ args: { tenantId: v.string(), viewerId: v.id("users") }, handler: async (ctx, { tenantId, viewerId }) => { await requireStaff(ctx, viewerId, tenantId); const queues = await ctx.db.query("queues").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).collect(); const result = await Promise.all( queues.map(async (qItem) => { const pending = await ctx.db .query("tickets") .withIndex("by_tenant_queue", (q) => q.eq("tenantId", tenantId).eq("queueId", qItem._id)) .collect(); const waiting = pending.filter((t) => t.status === "PENDING" || t.status === "ON_HOLD").length; const open = pending.filter((t) => t.status !== "RESOLVED" && t.status !== "CLOSED").length; const breached = 0; return { id: qItem._id, name: renameQueueString(qItem.name), pending: open, waiting, breached }; }) ); return result; }, }); export const create = mutation({ args: { tenantId: v.string(), actorId: v.id("users"), name: v.string(), teamId: v.optional(v.id("teams")), }, handler: async (ctx, { tenantId, actorId, name, teamId }) => { await requireAdmin(ctx, actorId, tenantId); const trimmed = name.trim(); if (trimmed.length < 2) { throw new ConvexError("Informe um nome válido para a fila"); } await ensureUniqueName(ctx, tenantId, trimmed); const slug = slugify(trimmed); await ensureUniqueSlug(ctx, tenantId, slug); if (teamId) { const team = await ctx.db.get(teamId); if (!team || team.tenantId !== tenantId) { throw new ConvexError("Time inválido"); } } const id = await ctx.db.insert("queues", { tenantId, name: trimmed, slug, teamId: teamId ?? undefined, }); return id; }, }); export const update = mutation({ args: { queueId: v.id("queues"), tenantId: v.string(), actorId: v.id("users"), name: v.string(), teamId: v.optional(v.id("teams")), }, handler: async (ctx, { queueId, tenantId, actorId, name, teamId }) => { await requireAdmin(ctx, actorId, tenantId); const queue = await ctx.db.get(queueId); if (!queue || queue.tenantId !== tenantId) { throw new ConvexError("Fila não encontrada"); } const trimmed = name.trim(); if (trimmed.length < 2) { throw new ConvexError("Informe um nome válido para a fila"); } await ensureUniqueName(ctx, tenantId, trimmed, queueId); let slug = queue.slug; if (queue.name !== trimmed) { slug = slugify(trimmed); await ensureUniqueSlug(ctx, tenantId, slug, queueId); } if (teamId) { const team = await ctx.db.get(teamId); if (!team || team.tenantId !== tenantId) { throw new ConvexError("Time inválido"); } } await ctx.db.patch(queueId, { name: trimmed, slug, teamId: teamId ?? undefined, }); }, }); export const remove = mutation({ args: { queueId: v.id("queues"), tenantId: v.string(), actorId: v.id("users"), }, handler: async (ctx, { queueId, tenantId, actorId }) => { await requireAdmin(ctx, actorId, tenantId); const queue = await ctx.db.get(queueId); if (!queue || queue.tenantId !== tenantId) { throw new ConvexError("Fila não encontrada"); } const ticketUsingQueue = await ctx.db .query("tickets") .withIndex("by_tenant_queue", (q) => q.eq("tenantId", tenantId).eq("queueId", queueId)) .first(); if (ticketUsingQueue) { throw new ConvexError("Não é possível remover uma fila vinculada a tickets"); } await ctx.db.delete(queueId); }, });