feat: add company management and manager role support
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
parent
409cbea7b9
commit
854887f499
16 changed files with 955 additions and 126 deletions
|
|
@ -6,7 +6,8 @@ import type { MutationCtx, QueryCtx } from "./_generated/server"
|
|||
|
||||
const SECRET = process.env.CONVEX_SYNC_SECRET ?? "dev-sync-secret"
|
||||
|
||||
const STAFF_ROLES = new Set(["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"])
|
||||
const VALID_ROLES = new Set(["ADMIN", "MANAGER", "AGENT", "COLLABORATOR", "CUSTOMER"])
|
||||
const INTERNAL_STAFF_ROLES = new Set(["ADMIN", "AGENT", "COLLABORATOR"])
|
||||
|
||||
function normalizeEmail(value: string) {
|
||||
return value.trim().toLowerCase()
|
||||
|
|
@ -18,6 +19,7 @@ type ImportedUser = {
|
|||
role?: string | null
|
||||
avatarUrl?: string | null
|
||||
teams?: string[] | null
|
||||
companySlug?: string | null
|
||||
}
|
||||
|
||||
type ImportedQueue = {
|
||||
|
|
@ -25,11 +27,22 @@ type ImportedQueue = {
|
|||
name: string
|
||||
}
|
||||
|
||||
type ImportedCompany = {
|
||||
slug: string
|
||||
name: string
|
||||
cnpj?: string | null
|
||||
domain?: string | null
|
||||
phone?: string | null
|
||||
description?: string | null
|
||||
address?: string | null
|
||||
createdAt?: number | null
|
||||
updatedAt?: number | null
|
||||
}
|
||||
|
||||
function normalizeRole(role: string | null | undefined) {
|
||||
if (!role) return "AGENT"
|
||||
const normalized = role.toUpperCase()
|
||||
if (STAFF_ROLES.has(normalized)) return normalized
|
||||
if (normalized === "CUSTOMER") return "CUSTOMER"
|
||||
if (VALID_ROLES.has(normalized)) return normalized
|
||||
return "AGENT"
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +70,8 @@ async function ensureUser(
|
|||
ctx: MutationCtx,
|
||||
tenantId: string,
|
||||
data: ImportedUser,
|
||||
cache: Map<string, Id<"users">>
|
||||
cache: Map<string, Id<"users">>,
|
||||
companyCache: Map<string, Id<"companies">>
|
||||
) {
|
||||
if (cache.has(data.email)) {
|
||||
return cache.get(data.email)!
|
||||
|
|
@ -68,13 +82,15 @@ async function ensureUser(
|
|||
.first()
|
||||
|
||||
const role = normalizeRole(data.role)
|
||||
const companyId = data.companySlug ? companyCache.get(data.companySlug) : undefined
|
||||
const record = existing
|
||||
? (() => {
|
||||
const needsPatch =
|
||||
existing.name !== data.name ||
|
||||
existing.role !== role ||
|
||||
existing.avatarUrl !== (data.avatarUrl ?? undefined) ||
|
||||
JSON.stringify(existing.teams ?? []) !== JSON.stringify(data.teams ?? [])
|
||||
JSON.stringify(existing.teams ?? []) !== JSON.stringify(data.teams ?? []) ||
|
||||
(existing.companyId ?? undefined) !== companyId
|
||||
if (needsPatch) {
|
||||
return ctx.db.patch(existing._id, {
|
||||
name: data.name,
|
||||
|
|
@ -82,6 +98,7 @@ async function ensureUser(
|
|||
avatarUrl: data.avatarUrl ?? undefined,
|
||||
teams: data.teams ?? undefined,
|
||||
tenantId,
|
||||
companyId,
|
||||
})
|
||||
}
|
||||
return Promise.resolve()
|
||||
|
|
@ -93,6 +110,7 @@ async function ensureUser(
|
|||
role,
|
||||
avatarUrl: data.avatarUrl ?? undefined,
|
||||
teams: data.teams ?? undefined,
|
||||
companyId,
|
||||
})
|
||||
|
||||
const id = existing ? existing._id : ((await record) as Id<"users">)
|
||||
|
|
@ -144,6 +162,64 @@ async function ensureQueue(
|
|||
return id
|
||||
}
|
||||
|
||||
async function ensureCompany(
|
||||
ctx: MutationCtx,
|
||||
tenantId: string,
|
||||
data: ImportedCompany,
|
||||
cache: Map<string, Id<"companies">>
|
||||
) {
|
||||
const slug = data.slug || slugify(data.name)
|
||||
if (cache.has(slug)) {
|
||||
return cache.get(slug)!
|
||||
}
|
||||
|
||||
const existing = await ctx.db
|
||||
.query("companies")
|
||||
.withIndex("by_tenant_slug", (q) => q.eq("tenantId", tenantId).eq("slug", slug))
|
||||
.first()
|
||||
|
||||
const payload = pruneUndefined({
|
||||
tenantId,
|
||||
name: data.name,
|
||||
slug,
|
||||
cnpj: data.cnpj ?? undefined,
|
||||
domain: data.domain ?? undefined,
|
||||
phone: data.phone ?? undefined,
|
||||
description: data.description ?? undefined,
|
||||
address: data.address ?? undefined,
|
||||
createdAt: data.createdAt ?? Date.now(),
|
||||
updatedAt: data.updatedAt ?? Date.now(),
|
||||
})
|
||||
|
||||
let id: Id<"companies">
|
||||
if (existing) {
|
||||
const needsPatch =
|
||||
existing.name !== payload.name ||
|
||||
existing.cnpj !== (payload.cnpj ?? undefined) ||
|
||||
existing.domain !== (payload.domain ?? undefined) ||
|
||||
existing.phone !== (payload.phone ?? undefined) ||
|
||||
existing.description !== (payload.description ?? undefined) ||
|
||||
existing.address !== (payload.address ?? undefined)
|
||||
if (needsPatch) {
|
||||
await ctx.db.patch(existing._id, {
|
||||
name: payload.name,
|
||||
cnpj: payload.cnpj,
|
||||
domain: payload.domain,
|
||||
phone: payload.phone,
|
||||
description: payload.description,
|
||||
address: payload.address,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
}
|
||||
id = existing._id
|
||||
} else {
|
||||
id = await ctx.db.insert("companies", payload)
|
||||
}
|
||||
|
||||
cache.set(slug, id)
|
||||
return id
|
||||
}
|
||||
|
||||
async function getTenantUsers(ctx: QueryCtx, tenantId: string) {
|
||||
return ctx.db
|
||||
.query("users")
|
||||
|
|
@ -158,6 +234,13 @@ async function getTenantQueues(ctx: QueryCtx, tenantId: string) {
|
|||
.collect()
|
||||
}
|
||||
|
||||
async function getTenantCompanies(ctx: QueryCtx, tenantId: string) {
|
||||
return ctx.db
|
||||
.query("companies")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||
.collect()
|
||||
}
|
||||
|
||||
export const exportTenantSnapshot = query({
|
||||
args: {
|
||||
secret: v.string(),
|
||||
|
|
@ -168,10 +251,15 @@ export const exportTenantSnapshot = query({
|
|||
throw new ConvexError("CONVEX_SYNC_SECRET não configurada no backend")
|
||||
}
|
||||
|
||||
const [users, queues] = await Promise.all([getTenantUsers(ctx, tenantId), getTenantQueues(ctx, tenantId)])
|
||||
const [users, queues, companies] = await Promise.all([
|
||||
getTenantUsers(ctx, tenantId),
|
||||
getTenantQueues(ctx, tenantId),
|
||||
getTenantCompanies(ctx, tenantId),
|
||||
])
|
||||
|
||||
const userMap = new Map(users.map((user) => [user._id, user]))
|
||||
const queueMap = new Map(queues.map((queue) => [queue._id, queue]))
|
||||
const companyMap = new Map(companies.map((company) => [company._id, company]))
|
||||
|
||||
const tickets = await ctx.db
|
||||
.query("tickets")
|
||||
|
|
@ -194,6 +282,11 @@ export const exportTenantSnapshot = query({
|
|||
const requester = userMap.get(ticket.requesterId)
|
||||
const assignee = ticket.assigneeId ? userMap.get(ticket.assigneeId) : undefined
|
||||
const queue = ticket.queueId ? queueMap.get(ticket.queueId) : undefined
|
||||
const company = ticket.companyId
|
||||
? companyMap.get(ticket.companyId)
|
||||
: requester?.companyId
|
||||
? companyMap.get(requester.companyId)
|
||||
: undefined
|
||||
|
||||
if (!requester) {
|
||||
continue
|
||||
|
|
@ -209,6 +302,7 @@ export const exportTenantSnapshot = query({
|
|||
queueSlug: queue?.slug ?? undefined,
|
||||
requesterEmail: requester.email,
|
||||
assigneeEmail: assignee?.email ?? undefined,
|
||||
companySlug: company?.slug ?? undefined,
|
||||
dueAt: ticket.dueAt ?? undefined,
|
||||
firstResponseAt: ticket.firstResponseAt ?? undefined,
|
||||
resolvedAt: ticket.resolvedAt ?? undefined,
|
||||
|
|
@ -247,12 +341,24 @@ export const exportTenantSnapshot = query({
|
|||
|
||||
return {
|
||||
tenantId,
|
||||
companies: companies.map((company) => ({
|
||||
slug: company.slug,
|
||||
name: company.name,
|
||||
cnpj: company.cnpj ?? null,
|
||||
domain: company.domain ?? null,
|
||||
phone: company.phone ?? null,
|
||||
description: company.description ?? null,
|
||||
address: company.address ?? null,
|
||||
createdAt: company.createdAt,
|
||||
updatedAt: company.updatedAt,
|
||||
})),
|
||||
users: users.map((user) => ({
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role ?? null,
|
||||
avatarUrl: user.avatarUrl ?? null,
|
||||
teams: user.teams ?? [],
|
||||
companySlug: user.companyId ? companyMap.get(user.companyId)?.slug ?? null : null,
|
||||
})),
|
||||
queues: queues.map((queue) => ({
|
||||
name: queue.name,
|
||||
|
|
@ -268,6 +374,19 @@ export const importPrismaSnapshot = mutation({
|
|||
secret: v.string(),
|
||||
snapshot: v.object({
|
||||
tenantId: v.string(),
|
||||
companies: v.array(
|
||||
v.object({
|
||||
slug: v.string(),
|
||||
name: v.string(),
|
||||
cnpj: v.optional(v.string()),
|
||||
domain: v.optional(v.string()),
|
||||
phone: v.optional(v.string()),
|
||||
description: v.optional(v.string()),
|
||||
address: v.optional(v.string()),
|
||||
createdAt: v.optional(v.number()),
|
||||
updatedAt: v.optional(v.number()),
|
||||
})
|
||||
),
|
||||
users: v.array(
|
||||
v.object({
|
||||
email: v.string(),
|
||||
|
|
@ -275,6 +394,7 @@ export const importPrismaSnapshot = mutation({
|
|||
role: v.optional(v.string()),
|
||||
avatarUrl: v.optional(v.string()),
|
||||
teams: v.optional(v.array(v.string())),
|
||||
companySlug: v.optional(v.string()),
|
||||
})
|
||||
),
|
||||
queues: v.array(
|
||||
|
|
@ -294,6 +414,7 @@ export const importPrismaSnapshot = mutation({
|
|||
queueSlug: v.optional(v.string()),
|
||||
requesterEmail: v.string(),
|
||||
assigneeEmail: v.optional(v.string()),
|
||||
companySlug: v.optional(v.string()),
|
||||
dueAt: v.optional(v.number()),
|
||||
firstResponseAt: v.optional(v.number()),
|
||||
resolvedAt: v.optional(v.number()),
|
||||
|
|
@ -329,11 +450,16 @@ export const importPrismaSnapshot = mutation({
|
|||
throw new ConvexError("Segredo inválido para sincronização")
|
||||
}
|
||||
|
||||
const companyCache = new Map<string, Id<"companies">>()
|
||||
const userCache = new Map<string, Id<"users">>()
|
||||
const queueCache = new Map<string, Id<"queues">>()
|
||||
|
||||
for (const company of snapshot.companies) {
|
||||
await ensureCompany(ctx, snapshot.tenantId, company, companyCache)
|
||||
}
|
||||
|
||||
for (const user of snapshot.users) {
|
||||
await ensureUser(ctx, snapshot.tenantId, user, userCache)
|
||||
await ensureUser(ctx, snapshot.tenantId, user, userCache, companyCache)
|
||||
}
|
||||
|
||||
for (const queue of snapshot.queues) {
|
||||
|
|
@ -342,7 +468,7 @@ export const importPrismaSnapshot = mutation({
|
|||
|
||||
const snapshotStaffEmails = new Set(
|
||||
snapshot.users
|
||||
.filter((user) => normalizeRole(user.role ?? null) !== "CUSTOMER")
|
||||
.filter((user) => INTERNAL_STAFF_ROLES.has(normalizeRole(user.role ?? null)))
|
||||
.map((user) => normalizeEmail(user.email))
|
||||
)
|
||||
|
||||
|
|
@ -353,7 +479,7 @@ export const importPrismaSnapshot = mutation({
|
|||
|
||||
for (const user of existingTenantUsers) {
|
||||
const role = normalizeRole(user.role ?? null)
|
||||
if (STAFF_ROLES.has(role) && !snapshotStaffEmails.has(normalizeEmail(user.email))) {
|
||||
if (INTERNAL_STAFF_ROLES.has(role) && !snapshotStaffEmails.has(normalizeEmail(user.email))) {
|
||||
await ctx.db.delete(user._id)
|
||||
}
|
||||
}
|
||||
|
|
@ -370,7 +496,8 @@ export const importPrismaSnapshot = mutation({
|
|||
email: ticket.requesterEmail,
|
||||
name: ticket.requesterEmail,
|
||||
},
|
||||
userCache
|
||||
userCache,
|
||||
companyCache
|
||||
)
|
||||
const assigneeId = ticket.assigneeEmail
|
||||
? await ensureUser(
|
||||
|
|
@ -380,11 +507,13 @@ export const importPrismaSnapshot = mutation({
|
|||
email: ticket.assigneeEmail,
|
||||
name: ticket.assigneeEmail,
|
||||
},
|
||||
userCache
|
||||
userCache,
|
||||
companyCache
|
||||
)
|
||||
: undefined
|
||||
|
||||
const queueId = ticket.queueSlug ? queueCache.get(ticket.queueSlug) ?? (await ensureQueue(ctx, snapshot.tenantId, { name: ticket.queueSlug, slug: ticket.queueSlug }, queueCache)) : undefined
|
||||
const companyId = ticket.companySlug ? companyCache.get(ticket.companySlug) ?? (await ensureCompany(ctx, snapshot.tenantId, { slug: ticket.companySlug, name: ticket.companySlug }, companyCache)) : undefined
|
||||
|
||||
const existing = await ctx.db
|
||||
.query("tickets")
|
||||
|
|
@ -406,6 +535,7 @@ export const importPrismaSnapshot = mutation({
|
|||
assigneeId: assigneeId as Id<"users"> | undefined,
|
||||
working: false,
|
||||
slaPolicyId: undefined,
|
||||
companyId: companyId as Id<"companies"> | undefined,
|
||||
dueAt: ticket.dueAt ?? undefined,
|
||||
firstResponseAt: ticket.firstResponseAt ?? undefined,
|
||||
resolvedAt: ticket.resolvedAt ?? undefined,
|
||||
|
|
@ -452,7 +582,8 @@ export const importPrismaSnapshot = mutation({
|
|||
email: comment.authorEmail,
|
||||
name: comment.authorEmail,
|
||||
},
|
||||
userCache
|
||||
userCache,
|
||||
companyCache
|
||||
)
|
||||
await ctx.db.insert("ticketComments", {
|
||||
ticketId,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue