feat: enable assignee selection when creating tickets

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
rever-tecnologia 2025-10-06 11:36:19 -03:00
parent fe7025d433
commit be27dcfd15
4 changed files with 182 additions and 16 deletions

View file

@ -5,6 +5,8 @@ import { Id, type Doc } from "./_generated/dataModel";
import { requireCustomer, requireStaff, requireUser } from "./rbac";
const STAFF_ROLES = new Set(["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"]);
const QUEUE_RENAME_LOOKUP: Record<string, string> = {
"Suporte N1": "Chamados",
"suporte-n1": "Chamados",
@ -459,6 +461,7 @@ export const create = mutation({
channel: v.string(),
queueId: v.optional(v.id("queues")),
requesterId: v.id("users"),
assigneeId: v.optional(v.id("users")),
categoryId: v.id("ticketCategories"),
subcategoryId: v.id("ticketSubcategories"),
customFields: v.optional(
@ -471,11 +474,34 @@ export const create = mutation({
),
},
handler: async (ctx, args) => {
const { role } = await requireUser(ctx, args.actorId, args.tenantId)
const { user: actorUser, role } = await requireUser(ctx, args.actorId, args.tenantId)
if (role === "CUSTOMER" && args.requesterId !== args.actorId) {
throw new ConvexError("Clientes só podem abrir chamados para si mesmos")
}
if (args.assigneeId && (!role || !STAFF_ROLES.has(role))) {
throw new ConvexError("Somente a equipe interna pode definir o responsável")
}
let initialAssigneeId: Id<"users"> | undefined
let initialAssignee: Doc<"users"> | null = null
if (args.assigneeId) {
const assignee = (await ctx.db.get(args.assigneeId)) as Doc<"users"> | null
if (!assignee || assignee.tenantId !== args.tenantId) {
throw new ConvexError("Responsável inválido")
}
const normalizedAssigneeRole = (assignee.role ?? "AGENT").toUpperCase()
if (!STAFF_ROLES.has(normalizedAssigneeRole)) {
throw new ConvexError("Responsável inválido")
}
initialAssigneeId = assignee._id
initialAssignee = assignee
} else if (role && STAFF_ROLES.has(role)) {
initialAssigneeId = actorUser._id
initialAssignee = actorUser
}
const subject = args.subject.trim();
if (subject.length < 3) {
throw new ConvexError("Informe um assunto com pelo menos 3 caracteres");
@ -510,7 +536,7 @@ export const create = mutation({
categoryId: args.categoryId,
subcategoryId: args.subcategoryId,
requesterId: args.requesterId,
assigneeId: undefined,
assigneeId: initialAssigneeId,
working: false,
activeSessionId: undefined,
totalWorkedMs: 0,
@ -531,6 +557,16 @@ export const create = mutation({
payload: { requesterId: args.requesterId, requesterName: requester?.name, requesterAvatar: requester?.avatarUrl },
createdAt: now,
});
if (initialAssigneeId && initialAssignee) {
await ctx.db.insert("ticketEvents", {
ticketId: id,
type: "ASSIGNEE_CHANGED",
payload: { assigneeId: initialAssigneeId, assigneeName: initialAssignee.name, actorId: args.actorId },
createdAt: now,
})
}
return id;
},
});
@ -558,10 +594,23 @@ export const addComment = mutation({
throw new ConvexError("Ticket não encontrado")
}
const author = (await ctx.db.get(args.authorId)) as Doc<"users"> | null
if (!author || author.tenantId !== ticket.tenantId) {
throw new ConvexError("Autor do comentário inválido")
}
const normalizedRole = (author.role ?? "AGENT").toUpperCase()
if (ticket.requesterId === args.authorId) {
await requireCustomer(ctx, args.authorId, ticket.tenantId)
if (args.visibility !== "PUBLIC") {
throw new ConvexError("Clientes só podem registrar comentários públicos")
if (normalizedRole === "CUSTOMER") {
await requireCustomer(ctx, args.authorId, ticket.tenantId)
if (args.visibility !== "PUBLIC") {
throw new ConvexError("Clientes só podem registrar comentários públicos")
}
} else if (STAFF_ROLES.has(normalizedRole)) {
await requireStaff(ctx, args.authorId, ticket.tenantId)
} else {
throw new ConvexError("Autor não possui permissão para comentar")
}
} else {
await requireStaff(ctx, args.authorId, ticket.tenantId)
@ -577,11 +626,10 @@ export const addComment = mutation({
createdAt: now,
updatedAt: now,
});
const author = await ctx.db.get(args.authorId);
await ctx.db.insert("ticketEvents", {
ticketId: args.ticketId,
type: "COMMENT_ADDED",
payload: { authorId: args.authorId, authorName: author?.name, authorAvatar: author?.avatarUrl },
payload: { authorId: args.authorId, authorName: author.name, authorAvatar: author.avatarUrl },
createdAt: now,
});
// bump ticket updatedAt

View file

@ -1,5 +1,8 @@
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { ConvexError, v } from "convex/values";
import { requireAdmin } from "./rbac";
const STAFF_ROLES = new Set(["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"]);
export const ensureUser = mutation({
args: {
@ -69,11 +72,41 @@ export const ensureUser = mutation({
export const listAgents = query({
args: { tenantId: v.string() },
handler: async (ctx, { tenantId }) => {
const agents = await ctx.db
const users = await ctx.db
.query("users")
.withIndex("by_tenant_role", (q) => q.eq("tenantId", tenantId).eq("role", "AGENT"))
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect();
return agents;
return users
.filter((user) => {
const normalizedRole = (user.role ?? "AGENT").toUpperCase();
return STAFF_ROLES.has(normalizedRole);
})
.sort((a, b) => a.name.localeCompare(b.name, "pt-BR"));
},
});
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");
}
await ctx.db.delete(userId);
return { status: "deleted" };
},
});