Permite selecionar solicitante e empresa nos tickets

This commit is contained in:
codex-bot 2025-10-23 17:47:23 -03:00
parent 25321224a6
commit 4aee7d7719
6 changed files with 817 additions and 11 deletions

View file

@ -1550,6 +1550,95 @@ export const changeAssignee = mutation({
},
});
export const changeRequester = mutation({
args: { ticketId: v.id("tickets"), requesterId: v.id("users"), actorId: v.id("users") },
handler: async (ctx, { ticketId, requesterId, actorId }) => {
const ticket = await ctx.db.get(ticketId)
if (!ticket) {
throw new ConvexError("Ticket não encontrado")
}
const ticketDoc = ticket as Doc<"tickets">
const viewer = await requireTicketStaff(ctx, actorId, ticketDoc)
const viewerRole = (viewer.role ?? "AGENT").toUpperCase()
const actor = viewer.user
if (String(ticketDoc.requesterId) === String(requesterId)) {
return { status: "unchanged" }
}
const requester = (await ctx.db.get(requesterId)) as Doc<"users"> | null
if (!requester || requester.tenantId !== ticketDoc.tenantId) {
throw new ConvexError("Solicitante inválido")
}
if (viewerRole === "MANAGER") {
if (!actor.companyId) {
throw new ConvexError("Gestor não possui empresa vinculada")
}
if (requester.companyId !== actor.companyId) {
throw new ConvexError("Gestores só podem alterar para usuários da própria empresa")
}
}
const now = Date.now()
const requesterSnapshot = {
name: requester.name,
email: requester.email,
avatarUrl: requester.avatarUrl ?? undefined,
teams: requester.teams ?? undefined,
}
let companyId: Id<"companies"> | undefined
let companySnapshot: { name: string; slug?: string; isAvulso?: boolean } | undefined
if (requester.companyId) {
const company = await ctx.db.get(requester.companyId)
if (company) {
companyId = company._id as Id<"companies">
companySnapshot = {
name: company.name,
slug: company.slug ?? undefined,
isAvulso: company.isAvulso ?? undefined,
}
}
}
const patch: Record<string, unknown> = {
requesterId,
requesterSnapshot,
updatedAt: now,
}
if (companyId) {
patch["companyId"] = companyId
patch["companySnapshot"] = companySnapshot
} else {
patch["companyId"] = undefined
patch["companySnapshot"] = undefined
}
await ctx.db.patch(ticketId, patch)
await ctx.db.insert("ticketEvents", {
ticketId,
type: "REQUESTER_CHANGED",
payload: {
requesterId,
requesterName: requester.name,
requesterEmail: requester.email,
companyId: companyId ?? null,
companyName: companySnapshot?.name ?? null,
actorId,
actorName: actor.name,
actorAvatar: actor.avatarUrl,
},
createdAt: now,
})
return { status: "updated" }
},
})
export const changeQueue = mutation({
args: { ticketId: v.id("tickets"), queueId: v.id("queues"), actorId: v.id("users") },
handler: async (ctx, { ticketId, queueId, actorId }) => {

View file

@ -1,8 +1,10 @@
import { mutation, query } from "./_generated/server";
import { ConvexError, v } from "convex/values";
import { requireAdmin } from "./rbac";
import type { Id } from "./_generated/dataModel";
import { requireAdmin, requireStaff } from "./rbac";
const INTERNAL_STAFF_ROLES = new Set(["ADMIN", "AGENT"]);
const CUSTOMER_ROLES = new Set(["COLLABORATOR", "MANAGER"]);
export const ensureUser = mutation({
args: {
@ -88,6 +90,74 @@ export const listAgents = query({
},
});
export const listCustomers = query({
args: { tenantId: v.string(), viewerId: v.id("users") },
handler: async (ctx, { tenantId, viewerId }) => {
const viewer = await requireStaff(ctx, viewerId, tenantId)
const viewerRole = (viewer.role ?? "AGENT").toUpperCase()
let managerCompanyId: Id<"companies"> | null = null
if (viewerRole === "MANAGER") {
managerCompanyId = viewer.user.companyId ?? null
if (!managerCompanyId) {
throw new ConvexError("Gestor não possui empresa vinculada")
}
}
const users = await ctx.db
.query("users")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect();
const allowed = users.filter((user) => {
const role = (user.role ?? "COLLABORATOR").toUpperCase()
if (!CUSTOMER_ROLES.has(role)) return false
if (managerCompanyId && user.companyId !== managerCompanyId) return false
return true
})
const companyIds = Array.from(
new Set(
allowed
.map((user) => user.companyId)
.filter((companyId): companyId is Id<"companies"> => Boolean(companyId))
)
)
const companyMap = new Map<string, { name: string; isAvulso?: boolean | null }>()
if (companyIds.length > 0) {
await Promise.all(
companyIds.map(async (companyId) => {
const company = await ctx.db.get(companyId)
if (company) {
companyMap.set(String(companyId), {
name: company.name,
isAvulso: company.isAvulso ?? undefined,
})
}
})
)
}
return allowed
.map((user) => {
const companyId = user.companyId ? String(user.companyId) : null
const company = companyId ? companyMap.get(companyId) ?? null : null
return {
id: String(user._id),
name: user.name,
email: user.email,
role: (user.role ?? "COLLABORATOR").toUpperCase(),
companyId,
companyName: company?.name ?? null,
companyIsAvulso: Boolean(company?.isAvulso),
avatarUrl: user.avatarUrl ?? null,
}
})
.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 }) => {