sistema-de-chamados/convex/users.ts

166 lines
5.4 KiB
TypeScript

import { mutation, query } from "./_generated/server";
import { ConvexError, v } from "convex/values";
import { requireAdmin } from "./rbac";
const INTERNAL_STAFF_ROLES = new Set(["ADMIN", "AGENT"]);
export const ensureUser = mutation({
args: {
tenantId: v.string(),
email: v.string(),
name: v.string(),
avatarUrl: v.optional(v.string()),
role: v.optional(v.string()),
teams: v.optional(v.array(v.string())),
companyId: v.optional(v.id("companies")),
},
handler: async (ctx, args) => {
const existing = await ctx.db
.query("users")
.withIndex("by_tenant_email", (q) => q.eq("tenantId", args.tenantId).eq("email", args.email))
.first();
const reconcile = async (record: typeof existing) => {
if (!record) return null;
const shouldPatch =
record.tenantId !== args.tenantId ||
(args.role && record.role !== args.role) ||
(args.avatarUrl && record.avatarUrl !== args.avatarUrl) ||
record.name !== args.name ||
(args.teams && JSON.stringify(args.teams) !== JSON.stringify(record.teams ?? [])) ||
(args.companyId && record.companyId !== args.companyId);
if (shouldPatch) {
await ctx.db.patch(record._id, {
tenantId: args.tenantId,
role: args.role ?? record.role,
avatarUrl: args.avatarUrl ?? record.avatarUrl,
name: args.name,
teams: args.teams ?? record.teams,
companyId: args.companyId ?? record.companyId,
});
const updated = await ctx.db.get(record._id);
if (updated) {
return updated;
}
}
return record;
};
if (existing) {
const reconciled = await reconcile(existing);
if (reconciled) {
return reconciled;
}
} else {
const anyTenant = (await ctx.db.query("users").collect()).find((user) => user.email === args.email);
if (anyTenant) {
const reconciled = await reconcile(anyTenant);
if (reconciled) {
return reconciled;
}
}
}
const id = await ctx.db.insert("users", {
tenantId: args.tenantId,
email: args.email,
name: args.name,
avatarUrl: args.avatarUrl,
role: args.role ?? "AGENT",
teams: args.teams ?? [],
companyId: args.companyId,
});
return await ctx.db.get(id);
},
});
export const listAgents = query({
args: { tenantId: v.string() },
handler: async (ctx, { tenantId }) => {
const users = await ctx.db
.query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect();
// Only internal staff (ADMIN/AGENT) should appear as responsáveis
return users
.filter((user) => INTERNAL_STAFF_ROLES.has((user.role ?? "AGENT").toUpperCase()))
.sort((a, b) => a.name.localeCompare(b.name, "pt-BR"));
},
});
export const findByEmail = query({
args: { tenantId: v.string(), email: v.string() },
handler: async (ctx, { tenantId, email }) => {
const record = await ctx.db
.query("users")
.withIndex("by_tenant_email", (q) => q.eq("tenantId", tenantId).eq("email", email))
.first();
return record ?? null;
},
});
export const deleteUser = mutation({
args: { userId: v.id("users"), actorId: v.id("users") },
handler: async (ctx, { userId, actorId }) => {
const user = await ctx.db.get(userId);
if (!user) {
return { status: "not_found" };
}
await requireAdmin(ctx, actorId, user.tenantId);
const assignedTickets = await ctx.db
.query("tickets")
.withIndex("by_tenant_assignee", (q) => q.eq("tenantId", user.tenantId).eq("assigneeId", userId))
.take(1);
if (assignedTickets.length > 0) {
throw new ConvexError("Usuário ainda está atribuído a tickets");
}
const comments = await ctx.db
.query("ticketComments")
.withIndex("by_author", (q) => q.eq("authorId", userId))
.collect();
if (comments.length > 0) {
const authorSnapshot = {
name: user.name,
email: user.email,
avatarUrl: user.avatarUrl ?? undefined,
teams: user.teams ?? undefined,
};
await Promise.all(
comments.map(async (comment) => {
const existingSnapshot = comment.authorSnapshot;
const shouldUpdate =
!existingSnapshot ||
existingSnapshot.name !== authorSnapshot.name ||
existingSnapshot.email !== authorSnapshot.email ||
existingSnapshot.avatarUrl !== authorSnapshot.avatarUrl ||
JSON.stringify(existingSnapshot.teams ?? []) !== JSON.stringify(authorSnapshot.teams ?? []);
if (shouldUpdate) {
await ctx.db.patch(comment._id, { authorSnapshot });
}
}),
);
}
await ctx.db.delete(userId);
return { status: "deleted" };
},
});
export const assignCompany = mutation({
args: { tenantId: v.string(), email: v.string(), companyId: v.id("companies"), actorId: v.id("users") },
handler: async (ctx, { tenantId, email, companyId, actorId }) => {
await requireAdmin(ctx, actorId, tenantId)
const user = await ctx.db
.query("users")
.withIndex("by_tenant_email", (q) => q.eq("tenantId", tenantId).eq("email", email))
.first()
if (!user) throw new ConvexError("Usuário não encontrado no Convex")
await ctx.db.patch(user._id, { companyId })
const updated = await ctx.db.get(user._id)
return updated
},
})