chore: reorganize project structure and ensure default queues

This commit is contained in:
Esdras Renan 2025-10-06 22:59:35 -03:00
parent 854887f499
commit 1cccb852a5
201 changed files with 417 additions and 838 deletions

232
convex/teams.ts Normal file
View file

@ -0,0 +1,232 @@
import { mutation, query } from "./_generated/server";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import type { Id } from "./_generated/dataModel";
import { ConvexError, v } from "convex/values";
import { requireAdmin } from "./rbac";
function normalizeName(value: string) {
return value.trim();
}
type AnyCtx = QueryCtx | MutationCtx;
async function ensureUniqueName(ctx: AnyCtx, tenantId: string, name: string, excludeId?: Id<"teams">) {
const existing = await ctx.db
.query("teams")
.withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId).eq("name", name))
.first();
if (existing && (!excludeId || existing._id !== excludeId)) {
throw new ConvexError("Já existe um time com este nome");
}
}
export const list = query({
args: { tenantId: v.string(), viewerId: v.id("users") },
handler: async (ctx, { tenantId, viewerId }) => {
await requireAdmin(ctx, viewerId, tenantId);
const teams = await ctx.db
.query("teams")
.withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId))
.collect();
const users = await ctx.db
.query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect();
const queues = await ctx.db
.query("queues")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect();
return teams.map((team) => {
const members = users
.filter((user) => (user.teams ?? []).includes(team.name))
.map((user) => ({
id: user._id,
name: user.name,
email: user.email,
role: user.role ?? "AGENT",
}));
const linkedQueues = queues.filter((queue) => queue.teamId === team._id);
return {
id: team._id,
name: team.name,
description: team.description ?? "",
members,
queueCount: linkedQueues.length,
createdAt: team._creationTime,
};
});
},
});
export const create = mutation({
args: {
tenantId: v.string(),
actorId: v.id("users"),
name: v.string(),
description: v.optional(v.string()),
},
handler: async (ctx, { tenantId, actorId, name, description }) => {
await requireAdmin(ctx, actorId, tenantId);
const trimmed = normalizeName(name);
if (trimmed.length < 2) {
throw new ConvexError("Informe um nome válido para o time");
}
await ensureUniqueName(ctx, tenantId, trimmed);
const id = await ctx.db.insert("teams", {
tenantId,
name: trimmed,
description,
});
return id;
},
});
export const update = mutation({
args: {
teamId: v.id("teams"),
tenantId: v.string(),
actorId: v.id("users"),
name: v.string(),
description: v.optional(v.string()),
},
handler: async (ctx, { teamId, tenantId, actorId, name, description }) => {
await requireAdmin(ctx, actorId, tenantId);
const team = await ctx.db.get(teamId);
if (!team || team.tenantId !== tenantId) {
throw new ConvexError("Time não encontrado");
}
const trimmed = normalizeName(name);
if (trimmed.length < 2) {
throw new ConvexError("Informe um nome válido para o time");
}
await ensureUniqueName(ctx, tenantId, trimmed, teamId);
if (team.name !== trimmed) {
const users = await ctx.db
.query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect();
const now = users
.filter((user) => (user.teams ?? []).includes(team.name))
.map(async (user) => {
const teams = (user.teams ?? []).map((entry) => (entry === team.name ? trimmed : entry));
await ctx.db.patch(user._id, { teams });
});
await Promise.all(now);
}
await ctx.db.patch(teamId, { name: trimmed, description });
},
});
export const remove = mutation({
args: {
teamId: v.id("teams"),
tenantId: v.string(),
actorId: v.id("users"),
},
handler: async (ctx, { teamId, tenantId, actorId }) => {
await requireAdmin(ctx, actorId, tenantId);
const team = await ctx.db.get(teamId);
if (!team || team.tenantId !== tenantId) {
throw new ConvexError("Time não encontrado");
}
const queuesLinked = await ctx.db
.query("queues")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.filter((q) => q.eq(q.field("teamId"), teamId))
.first();
if (queuesLinked) {
throw new ConvexError("Remova ou realoque as filas associadas antes de excluir o time");
}
const users = await ctx.db
.query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect();
await Promise.all(
users
.filter((user) => (user.teams ?? []).includes(team.name))
.map((user) => {
const teams = (user.teams ?? []).filter((entry) => entry !== team.name);
return ctx.db.patch(user._id, { teams });
})
);
await ctx.db.delete(teamId);
},
});
export const setMembers = mutation({
args: {
teamId: v.id("teams"),
tenantId: v.string(),
actorId: v.id("users"),
memberIds: v.array(v.id("users")),
},
handler: async (ctx, { teamId, tenantId, actorId, memberIds }) => {
await requireAdmin(ctx, actorId, tenantId);
const team = await ctx.db.get(teamId);
if (!team || team.tenantId !== tenantId) {
throw new ConvexError("Time não encontrado");
}
const users = await ctx.db
.query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect();
const tenantUserIds = new Set(users.map((user) => user._id));
for (const memberId of memberIds) {
if (!tenantUserIds.has(memberId)) {
throw new ConvexError("Usuário inválido para este tenant");
}
}
const target = new Set(memberIds);
await Promise.all(
users.map(async (user) => {
const teams = new Set(user.teams ?? []);
const hasTeam = teams.has(team.name);
const shouldHave = target.has(user._id);
if (shouldHave && !hasTeam) {
teams.add(team.name);
await ctx.db.patch(user._id, { teams: Array.from(teams) });
}
if (!shouldHave && hasTeam) {
teams.delete(team.name);
await ctx.db.patch(user._id, { teams: Array.from(teams) });
}
})
);
},
});
export const directory = query({
args: { tenantId: v.string(), viewerId: v.id("users") },
handler: async (ctx, { tenantId, viewerId }) => {
await requireAdmin(ctx, viewerId, tenantId);
const users = await ctx.db
.query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect();
return users.map((user) => ({
id: user._id,
name: user.name,
email: user.email,
role: user.role ?? "AGENT",
teams: user.teams ?? [],
}));
},
});