sistema-de-chamados/convex/deviceExportTemplates.ts

347 lines
10 KiB
TypeScript

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,
})
},
})