feat: dispositivos e ajustes de csat e relatórios
This commit is contained in:
parent
25d2a9b062
commit
e0ef66555d
86 changed files with 5811 additions and 992 deletions
347
convex/deviceExportTemplates.ts
Normal file
347
convex/deviceExportTemplates.ts
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
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,
|
||||
})
|
||||
},
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue