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, requireUser } from "./rbac" type AnyCtx = MutationCtx | QueryCtx function normalizeSlug(input: string) { return input .trim() .toLowerCase() .normalize("NFD") .replace(/[^\w\s-]/g, "") .replace(/\s+/g, "-") .replace(/-+/g, "-") } async function ensureUniqueSlug(ctx: AnyCtx, tenantId: string, slug: string, excludeId?: Id<"deviceExportTemplates">) { const existing = await ctx.db .query("deviceExportTemplates") .withIndex("by_tenant_slug", (q) => q.eq("tenantId", tenantId).eq("slug", slug)) .first() if (existing && (!excludeId || existing._id !== excludeId)) { throw new ConvexError("Já existe um template com este identificador") } } async function unsetDefaults( ctx: MutationCtx, tenantId: string, companyId: Id<"companies"> | undefined | null, excludeId?: Id<"deviceExportTemplates"> ) { const templates = await ctx.db .query("deviceExportTemplates") .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .collect() await Promise.all( templates .filter((tpl) => tpl._id !== excludeId) .filter((tpl) => { if (companyId) { return tpl.companyId === companyId } return !tpl.companyId }) .map((tpl) => ctx.db.patch(tpl._id, { isDefault: false })) ) } function normalizeColumns(columns: { key: string; label?: string | null }[]) { return columns .map((col) => ({ key: col.key.trim(), label: col.label?.trim() || undefined, })) .filter((col) => col.key.length > 0) } export const list = query({ args: { tenantId: v.string(), viewerId: v.id("users"), companyId: v.optional(v.id("companies")), includeInactive: v.optional(v.boolean()), }, handler: async (ctx, { tenantId, viewerId, companyId, includeInactive }) => { await requireAdmin(ctx, viewerId, tenantId) const templates = await ctx.db .query("deviceExportTemplates") .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .collect() return templates .filter((tpl) => { if (!includeInactive && tpl.isActive === false) return false if (!companyId) return true if (!tpl.companyId) return true return tpl.companyId === companyId }) .sort((a, b) => a.name.localeCompare(b.name, "pt-BR")) .map((tpl) => ({ id: tpl._id, slug: tpl.slug, name: tpl.name, description: tpl.description ?? "", columns: tpl.columns ?? [], filters: tpl.filters ?? null, companyId: tpl.companyId ?? null, isDefault: Boolean(tpl.isDefault), isActive: tpl.isActive ?? true, createdAt: tpl.createdAt, updatedAt: tpl.updatedAt, createdBy: tpl.createdBy ?? null, updatedBy: tpl.updatedBy ?? null, })) }, }) export const listForTenant = query({ args: { tenantId: v.string(), viewerId: v.id("users"), companyId: v.optional(v.id("companies")), }, handler: async (ctx, { tenantId, viewerId, companyId }) => { await requireUser(ctx, viewerId, tenantId) const templates = await ctx.db .query("deviceExportTemplates") .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .collect() return templates .filter((tpl) => tpl.isActive !== false) .filter((tpl) => { if (!companyId) return !tpl.companyId return !tpl.companyId || tpl.companyId === companyId }) .sort((a, b) => a.name.localeCompare(b.name, "pt-BR")) .map((tpl) => ({ id: tpl._id, slug: tpl.slug, name: tpl.name, description: tpl.description ?? "", columns: tpl.columns ?? [], filters: tpl.filters ?? null, companyId: tpl.companyId ?? null, isDefault: Boolean(tpl.isDefault), isActive: tpl.isActive ?? true, })) }, }) export const getDefault = query({ args: { tenantId: v.string(), viewerId: v.id("users"), companyId: v.optional(v.id("companies")), }, handler: async (ctx, { tenantId, viewerId, companyId }) => { await requireUser(ctx, viewerId, tenantId) const indexQuery = companyId ? ctx.db .query("deviceExportTemplates") .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId)) : ctx.db.query("deviceExportTemplates").withIndex("by_tenant_default", (q) => q.eq("tenantId", tenantId).eq("isDefault", true)) const templates = await indexQuery.collect() const candidate = templates.find((tpl) => tpl.isDefault) ?? null if (candidate) { return { id: candidate._id, slug: candidate.slug, name: candidate.name, description: candidate.description ?? "", columns: candidate.columns ?? [], filters: candidate.filters ?? null, companyId: candidate.companyId ?? null, isDefault: Boolean(candidate.isDefault), isActive: candidate.isActive ?? true, } } if (companyId) { const globalDefault = await ctx.db .query("deviceExportTemplates") .withIndex("by_tenant_default", (q) => q.eq("tenantId", tenantId).eq("isDefault", true)) .first() if (globalDefault) { return { id: globalDefault._id, slug: globalDefault.slug, name: globalDefault.name, description: globalDefault.description ?? "", columns: globalDefault.columns ?? [], filters: globalDefault.filters ?? null, companyId: globalDefault.companyId ?? null, isDefault: Boolean(globalDefault.isDefault), isActive: globalDefault.isActive ?? true, } } } return null }, }) export const create = mutation({ args: { tenantId: v.string(), actorId: v.id("users"), name: v.string(), description: v.optional(v.string()), columns: v.array( v.object({ key: v.string(), label: v.optional(v.string()), }) ), filters: v.optional(v.any()), companyId: v.optional(v.id("companies")), isDefault: v.optional(v.boolean()), isActive: v.optional(v.boolean()), }, handler: async (ctx, args) => { await requireAdmin(ctx, args.actorId, args.tenantId) const normalizedName = args.name.trim() if (normalizedName.length < 3) { throw new ConvexError("Informe um nome para o template") } const slug = normalizeSlug(normalizedName) if (!slug) { throw new ConvexError("Não foi possível gerar um identificador para o template") } await ensureUniqueSlug(ctx, args.tenantId, slug) const columns = normalizeColumns(args.columns) if (columns.length === 0) { throw new ConvexError("Selecione ao menos uma coluna") } const now = Date.now() const templateId = await ctx.db.insert("deviceExportTemplates", { tenantId: args.tenantId, name: normalizedName, slug, description: args.description ?? undefined, columns, filters: args.filters ?? undefined, companyId: args.companyId ?? undefined, isDefault: Boolean(args.isDefault), isActive: args.isActive ?? true, createdBy: args.actorId, updatedBy: args.actorId, createdAt: now, updatedAt: now, }) if (args.isDefault) { await unsetDefaults(ctx, args.tenantId, args.companyId ?? null, templateId) } return templateId }, }) export const update = mutation({ args: { tenantId: v.string(), actorId: v.id("users"), templateId: v.id("deviceExportTemplates"), name: v.string(), description: v.optional(v.string()), columns: v.array( v.object({ key: v.string(), label: v.optional(v.string()), }) ), filters: v.optional(v.any()), companyId: v.optional(v.id("companies")), isDefault: v.optional(v.boolean()), isActive: v.optional(v.boolean()), }, handler: async (ctx, args) => { await requireAdmin(ctx, args.actorId, args.tenantId) const template = await ctx.db.get(args.templateId) if (!template || template.tenantId !== args.tenantId) { throw new ConvexError("Template não encontrado") } const normalizedName = args.name.trim() if (normalizedName.length < 3) { throw new ConvexError("Informe um nome para o template") } let slug = template.slug if (template.name !== normalizedName) { slug = normalizeSlug(normalizedName) if (!slug) throw new ConvexError("Não foi possível gerar um identificador para o template") await ensureUniqueSlug(ctx, args.tenantId, slug, args.templateId) } const columns = normalizeColumns(args.columns) if (columns.length === 0) { throw new ConvexError("Selecione ao menos uma coluna") } const isDefault = Boolean(args.isDefault) await ctx.db.patch(args.templateId, { name: normalizedName, slug, description: args.description ?? undefined, columns, filters: args.filters ?? undefined, companyId: args.companyId ?? undefined, isDefault, isActive: args.isActive ?? true, updatedAt: Date.now(), updatedBy: args.actorId, }) if (isDefault) { await unsetDefaults(ctx, args.tenantId, args.companyId ?? null, args.templateId) } }, }) export const remove = mutation({ args: { tenantId: v.string(), actorId: v.id("users"), templateId: v.id("deviceExportTemplates"), }, handler: async (ctx, args) => { await requireAdmin(ctx, args.actorId, args.tenantId) const template = await ctx.db.get(args.templateId) if (!template || template.tenantId !== args.tenantId) { throw new ConvexError("Template não encontrado") } await ctx.db.delete(args.templateId) }, }) export const setDefault = mutation({ args: { tenantId: v.string(), actorId: v.id("users"), templateId: v.id("deviceExportTemplates"), }, handler: async (ctx, args) => { await requireAdmin(ctx, args.actorId, args.tenantId) const template = await ctx.db.get(args.templateId) if (!template || template.tenantId !== args.tenantId) { throw new ConvexError("Template não encontrado") } await unsetDefaults(ctx, args.tenantId, template.companyId ?? null, args.templateId) await ctx.db.patch(args.templateId, { isDefault: true, updatedAt: Date.now(), updatedBy: args.actorId, }) }, }) export const clearCompanyDefault = 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 templates = await ctx.db .query("deviceExportTemplates") .withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId)) .collect() const now = Date.now() await Promise.all( templates.map((tpl) => ctx.db.patch(tpl._id, { isDefault: false, updatedAt: now, updatedBy: actorId, }) ) ) }, })