347 lines
10 KiB
TypeScript
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,
|
|
})
|
|
},
|
|
})
|