chore: reorganize project structure and ensure default queues
This commit is contained in:
parent
854887f499
commit
1cccb852a5
201 changed files with 417 additions and 838 deletions
47
scripts/debug-convex.mjs
Normal file
47
scripts/debug-convex.mjs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import "dotenv/config"
|
||||
import { ConvexHttpClient } from "convex/browser";
|
||||
|
||||
const url = process.env.NEXT_PUBLIC_CONVEX_URL;
|
||||
|
||||
if (!url) {
|
||||
console.error("Missing NEXT_PUBLIC_CONVEX_URL");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new ConvexHttpClient(url);
|
||||
|
||||
const tenantId = process.argv[2] ?? "tenant-atlas";
|
||||
|
||||
const ensureAdmin = await client.mutation("users:ensureUser", {
|
||||
tenantId,
|
||||
email: "admin@sistema.dev",
|
||||
name: "Administrador",
|
||||
role: "ADMIN",
|
||||
});
|
||||
|
||||
console.log("Ensured admin user:", ensureAdmin);
|
||||
|
||||
const agents = await client.query("users:listAgents", { tenantId });
|
||||
console.log("Agents:", agents);
|
||||
|
||||
const viewerId = ensureAdmin?._id ?? agents[0]?._id;
|
||||
|
||||
if (!viewerId) {
|
||||
console.error("Unable to determine viewer id");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tickets = await client.query("tickets:list", {
|
||||
tenantId,
|
||||
viewerId,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
console.log("Tickets:", tickets);
|
||||
|
||||
const dashboard = await client.query("reports:dashboardOverview", {
|
||||
tenantId,
|
||||
viewerId,
|
||||
});
|
||||
|
||||
console.log("Dashboard:", dashboard);
|
||||
73
scripts/ensure-default-queues.mjs
Normal file
73
scripts/ensure-default-queues.mjs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import "dotenv/config"
|
||||
import { ConvexHttpClient } from "convex/browser"
|
||||
|
||||
const tenantId = process.env.SYNC_TENANT_ID || "tenant-atlas"
|
||||
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL
|
||||
|
||||
if (!convexUrl) {
|
||||
console.error("NEXT_PUBLIC_CONVEX_URL não configurado. Ajuste o .env antes de executar o script.")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const DEFAULT_QUEUES = [
|
||||
{ name: "Chamados" },
|
||||
{ name: "Laboratório" },
|
||||
{ name: "Visitas" },
|
||||
]
|
||||
|
||||
function slugify(value) {
|
||||
return value
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^\w\s-]/g, "")
|
||||
.trim()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const client = new ConvexHttpClient(convexUrl)
|
||||
|
||||
const agents = await client.query("users:listAgents", { tenantId })
|
||||
const admin =
|
||||
agents.find((user) => (user.role ?? "").toUpperCase() === "ADMIN") ??
|
||||
agents[0]
|
||||
|
||||
if (!admin?._id) {
|
||||
console.error("Nenhum usuário ADMIN encontrado no Convex para criar filas padrão.")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const existing = await client.query("queues:list", {
|
||||
tenantId,
|
||||
viewerId: admin._id,
|
||||
})
|
||||
|
||||
const existingSlugs = new Set(existing.map((queue) => queue.slug))
|
||||
const created = []
|
||||
|
||||
for (const def of DEFAULT_QUEUES) {
|
||||
const slug = slugify(def.name)
|
||||
if (existingSlugs.has(slug)) {
|
||||
continue
|
||||
}
|
||||
await client.mutation("queues:create", {
|
||||
tenantId,
|
||||
actorId: admin._id,
|
||||
name: def.name,
|
||||
})
|
||||
created.push(def.name)
|
||||
}
|
||||
|
||||
if (created.length === 0) {
|
||||
console.log("Nenhuma fila criada. As filas padrão já existem.")
|
||||
} else {
|
||||
console.log(`Filas criadas: ${created.join(", ")}`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Falha ao garantir filas padrão", error)
|
||||
process.exit(1)
|
||||
})
|
||||
364
scripts/import-convex-to-prisma.mjs
Normal file
364
scripts/import-convex-to-prisma.mjs
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
import "dotenv/config"
|
||||
import { PrismaClient } from "@prisma/client"
|
||||
import { ConvexHttpClient } from "convex/browser"
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const tenantId = process.env.SYNC_TENANT_ID || "tenant-atlas"
|
||||
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL || "http://127.0.0.1:3210"
|
||||
const secret = process.env.CONVEX_SYNC_SECRET
|
||||
const STAFF_ROSTER = [
|
||||
{ email: "admin@sistema.dev", name: "Administrador", role: "ADMIN" },
|
||||
{ email: "gabriel.oliveira@rever.com.br", name: "Gabriel Oliveira", role: "AGENT" },
|
||||
{ email: "george.araujo@rever.com.br", name: "George Araujo", role: "AGENT" },
|
||||
{ email: "hugo.soares@rever.com.br", name: "Hugo Soares", role: "AGENT" },
|
||||
{ email: "julio@rever.com.br", name: "Julio Cesar", role: "AGENT" },
|
||||
{ email: "lorena@rever.com.br", name: "Lorena Magalhães", role: "AGENT" },
|
||||
{ email: "renan.pac@paulicon.com.br", name: "Rever", role: "AGENT" },
|
||||
{ email: "thiago.medeiros@rever.com.br", name: "Thiago Medeiros", role: "AGENT" },
|
||||
{ email: "weslei@rever.com.br", name: "Weslei Magalhães", role: "AGENT" },
|
||||
]
|
||||
|
||||
const rawDefaultAssigneeEmail = process.env.SYNC_DEFAULT_ASSIGNEE || "gabriel.oliveira@rever.com.br"
|
||||
|
||||
if (!secret) {
|
||||
console.error("CONVEX_SYNC_SECRET não configurado. Configure no .env.")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const allowedRoles = new Set(["ADMIN", "MANAGER", "AGENT", "COLLABORATOR", "CUSTOMER"])
|
||||
|
||||
const client = new ConvexHttpClient(convexUrl)
|
||||
|
||||
function normalizeEmail(email) {
|
||||
if (!email) return null
|
||||
return email.trim().toLowerCase()
|
||||
}
|
||||
|
||||
const defaultAssigneeEmail = normalizeEmail(rawDefaultAssigneeEmail)
|
||||
|
||||
function toDate(value) {
|
||||
if (!value && value !== 0) return null
|
||||
return new Date(value)
|
||||
}
|
||||
|
||||
function slugify(value) {
|
||||
return value
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^\w\s-]/g, "")
|
||||
.trim()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
async function upsertCompanies(snapshotCompanies) {
|
||||
const map = new Map()
|
||||
|
||||
for (const company of snapshotCompanies) {
|
||||
const slug = company.slug || slugify(company.name)
|
||||
const record = await prisma.company.upsert({
|
||||
where: {
|
||||
tenantId_slug: {
|
||||
tenantId,
|
||||
slug,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
name: company.name,
|
||||
cnpj: company.cnpj ?? null,
|
||||
domain: company.domain ?? null,
|
||||
phone: company.phone ?? null,
|
||||
description: company.description ?? null,
|
||||
address: company.address ?? null,
|
||||
},
|
||||
create: {
|
||||
tenantId,
|
||||
name: company.name,
|
||||
slug,
|
||||
cnpj: company.cnpj ?? null,
|
||||
domain: company.domain ?? null,
|
||||
phone: company.phone ?? null,
|
||||
description: company.description ?? null,
|
||||
address: company.address ?? null,
|
||||
createdAt: toDate(company.createdAt) ?? new Date(),
|
||||
updatedAt: toDate(company.updatedAt) ?? new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
map.set(slug, record.id)
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
async function upsertUsers(snapshotUsers, companyMap) {
|
||||
const map = new Map()
|
||||
|
||||
for (const user of snapshotUsers) {
|
||||
const normalizedEmail = normalizeEmail(user.email)
|
||||
if (!normalizedEmail) continue
|
||||
|
||||
const normalizedRole = (user.role ?? "CUSTOMER").toUpperCase()
|
||||
const role = allowedRoles.has(normalizedRole) ? normalizedRole : "CUSTOMER"
|
||||
const companyId = user.companySlug ? companyMap.get(user.companySlug) ?? null : null
|
||||
|
||||
const record = await prisma.user.upsert({
|
||||
where: { email: normalizedEmail },
|
||||
update: {
|
||||
name: user.name ?? normalizedEmail,
|
||||
role,
|
||||
tenantId,
|
||||
avatarUrl: user.avatarUrl ?? null,
|
||||
companyId,
|
||||
},
|
||||
create: {
|
||||
email: normalizedEmail,
|
||||
name: user.name ?? normalizedEmail,
|
||||
role,
|
||||
tenantId,
|
||||
avatarUrl: user.avatarUrl ?? null,
|
||||
companyId,
|
||||
},
|
||||
})
|
||||
|
||||
map.set(normalizedEmail, record.id)
|
||||
}
|
||||
|
||||
for (const staff of STAFF_ROSTER) {
|
||||
const normalizedEmail = normalizeEmail(staff.email)
|
||||
if (!normalizedEmail) continue
|
||||
const record = await prisma.user.upsert({
|
||||
where: { email: normalizedEmail },
|
||||
update: {
|
||||
name: staff.name,
|
||||
role: staff.role,
|
||||
tenantId,
|
||||
companyId: null,
|
||||
},
|
||||
create: {
|
||||
email: normalizedEmail,
|
||||
name: staff.name,
|
||||
role: staff.role,
|
||||
tenantId,
|
||||
avatarUrl: null,
|
||||
companyId: null,
|
||||
},
|
||||
})
|
||||
map.set(normalizedEmail, record.id)
|
||||
}
|
||||
|
||||
const allowedStaffEmails = new Set(STAFF_ROSTER.map((staff) => normalizeEmail(staff.email)).filter(Boolean))
|
||||
|
||||
const removableStaff = await prisma.user.findMany({
|
||||
where: {
|
||||
tenantId,
|
||||
role: { in: ["ADMIN", "AGENT", "COLLABORATOR"] },
|
||||
email: {
|
||||
notIn: Array.from(allowedStaffEmails),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const fallbackAssigneeId = defaultAssigneeEmail ? map.get(defaultAssigneeEmail) ?? null : null
|
||||
|
||||
for (const staff of removableStaff) {
|
||||
if (fallbackAssigneeId) {
|
||||
await prisma.ticket.updateMany({
|
||||
where: { tenantId, assigneeId: staff.id },
|
||||
data: { assigneeId: fallbackAssigneeId },
|
||||
})
|
||||
await prisma.ticketComment.updateMany({
|
||||
where: { authorId: staff.id },
|
||||
data: { authorId: fallbackAssigneeId },
|
||||
})
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: staff.id },
|
||||
data: {
|
||||
role: "CUSTOMER",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
async function upsertQueues(snapshotQueues) {
|
||||
const map = new Map()
|
||||
|
||||
for (const queue of snapshotQueues) {
|
||||
if (!queue.slug) continue
|
||||
const record = await prisma.queue.upsert({
|
||||
where: {
|
||||
tenantId_slug: {
|
||||
tenantId,
|
||||
slug: queue.slug,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
name: queue.name,
|
||||
},
|
||||
create: {
|
||||
tenantId,
|
||||
name: queue.name,
|
||||
slug: queue.slug,
|
||||
},
|
||||
})
|
||||
|
||||
map.set(queue.slug, record.id)
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
async function upsertTickets(snapshotTickets, userMap, queueMap, companyMap) {
|
||||
let created = 0
|
||||
let updated = 0
|
||||
|
||||
const fallbackAssigneeId = defaultAssigneeEmail ? userMap.get(defaultAssigneeEmail) ?? null : null
|
||||
|
||||
for (const ticket of snapshotTickets) {
|
||||
if (!ticket.requesterEmail) continue
|
||||
|
||||
const requesterId = userMap.get(normalizeEmail(ticket.requesterEmail))
|
||||
if (!requesterId) continue
|
||||
|
||||
const queueId = ticket.queueSlug ? queueMap.get(ticket.queueSlug) ?? null : null
|
||||
|
||||
let companyId = ticket.companySlug ? companyMap.get(ticket.companySlug) ?? null : null
|
||||
if (!companyId && requesterId) {
|
||||
const requester = await prisma.user.findUnique({
|
||||
where: { id: requesterId },
|
||||
select: { companyId: true },
|
||||
})
|
||||
companyId = requester?.companyId ?? null
|
||||
}
|
||||
|
||||
const desiredAssigneeEmail = defaultAssigneeEmail || normalizeEmail(ticket.assigneeEmail)
|
||||
const assigneeId = desiredAssigneeEmail ? userMap.get(desiredAssigneeEmail) || fallbackAssigneeId || null : fallbackAssigneeId || null
|
||||
|
||||
const existing = await prisma.ticket.findFirst({
|
||||
where: {
|
||||
tenantId,
|
||||
reference: ticket.reference,
|
||||
},
|
||||
})
|
||||
|
||||
const data = {
|
||||
subject: ticket.subject,
|
||||
summary: ticket.summary ?? null,
|
||||
status: (ticket.status ?? "NEW").toUpperCase(),
|
||||
priority: (ticket.priority ?? "MEDIUM").toUpperCase(),
|
||||
channel: (ticket.channel ?? "MANUAL").toUpperCase(),
|
||||
queueId,
|
||||
requesterId,
|
||||
assigneeId,
|
||||
dueAt: toDate(ticket.dueAt),
|
||||
firstResponseAt: toDate(ticket.firstResponseAt),
|
||||
resolvedAt: toDate(ticket.resolvedAt),
|
||||
closedAt: toDate(ticket.closedAt),
|
||||
createdAt: toDate(ticket.createdAt) ?? new Date(),
|
||||
updatedAt: toDate(ticket.updatedAt) ?? new Date(),
|
||||
companyId,
|
||||
}
|
||||
|
||||
let ticketRecord
|
||||
|
||||
if (existing) {
|
||||
ticketRecord = await prisma.ticket.update({
|
||||
where: { id: existing.id },
|
||||
data,
|
||||
})
|
||||
updated += 1
|
||||
} else {
|
||||
ticketRecord = await prisma.ticket.create({
|
||||
data: {
|
||||
tenantId,
|
||||
reference: ticket.reference,
|
||||
...data,
|
||||
},
|
||||
})
|
||||
created += 1
|
||||
}
|
||||
|
||||
await prisma.ticketComment.deleteMany({ where: { ticketId: ticketRecord.id } })
|
||||
await prisma.ticketEvent.deleteMany({ where: { ticketId: ticketRecord.id } })
|
||||
|
||||
const commentsData = ticket.comments
|
||||
.map((comment) => {
|
||||
const authorId = comment.authorEmail ? userMap.get(normalizeEmail(comment.authorEmail)) : null
|
||||
if (!authorId) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
ticketId: ticketRecord.id,
|
||||
authorId,
|
||||
visibility: (comment.visibility ?? "INTERNAL").toUpperCase(),
|
||||
body: comment.body ?? "",
|
||||
attachments: null,
|
||||
createdAt: toDate(comment.createdAt) ?? new Date(),
|
||||
updatedAt: toDate(comment.updatedAt) ?? new Date(),
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
if (commentsData.length > 0) {
|
||||
await prisma.ticketComment.createMany({ data: commentsData })
|
||||
}
|
||||
|
||||
const eventsData = ticket.events.map((event) => ({
|
||||
ticketId: ticketRecord.id,
|
||||
type: event.type ?? "UNKNOWN",
|
||||
payload: event.payload ?? {},
|
||||
createdAt: toDate(event.createdAt) ?? new Date(),
|
||||
}))
|
||||
|
||||
if (eventsData.length > 0) {
|
||||
await prisma.ticketEvent.createMany({ data: eventsData })
|
||||
}
|
||||
}
|
||||
|
||||
return { created, updated }
|
||||
}
|
||||
|
||||
async function run() {
|
||||
console.log("Baixando snapshot do Convex...")
|
||||
const snapshot = await client.query("migrations:exportTenantSnapshot", {
|
||||
secret,
|
||||
tenantId,
|
||||
})
|
||||
|
||||
console.log(`Empresas recebidas: ${snapshot.companies.length}`)
|
||||
console.log(`Usuários recebidos: ${snapshot.users.length}`)
|
||||
console.log(`Filas recebidas: ${snapshot.queues.length}`)
|
||||
console.log(`Tickets recebidos: ${snapshot.tickets.length}`)
|
||||
|
||||
console.log("Sincronizando empresas no Prisma...")
|
||||
const companyMap = await upsertCompanies(snapshot.companies)
|
||||
console.log(`Empresas ativas no mapa: ${companyMap.size}`)
|
||||
|
||||
console.log("Sincronizando usuários no Prisma...")
|
||||
const userMap = await upsertUsers(snapshot.users, companyMap)
|
||||
console.log(`Usuários ativos no mapa: ${userMap.size}`)
|
||||
|
||||
console.log("Sincronizando filas no Prisma...")
|
||||
const queueMap = await upsertQueues(snapshot.queues)
|
||||
console.log(`Filas ativas no mapa: ${queueMap.size}`)
|
||||
|
||||
console.log("Sincronizando tickets no Prisma...")
|
||||
const ticketStats = await upsertTickets(snapshot.tickets, userMap, queueMap, companyMap)
|
||||
console.log(`Tickets criados: ${ticketStats.created}`)
|
||||
console.log(`Tickets atualizados: ${ticketStats.updated}`)
|
||||
}
|
||||
|
||||
run()
|
||||
.catch((error) => {
|
||||
console.error("Falha ao importar dados do Convex para Prisma", error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
88
scripts/reassign-legacy-assignees.mjs
Normal file
88
scripts/reassign-legacy-assignees.mjs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import "dotenv/config"
|
||||
import { ConvexHttpClient } from "convex/browser"
|
||||
|
||||
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL
|
||||
const TENANT_ID = process.env.SEED_TENANT_ID ?? "tenant-atlas"
|
||||
|
||||
if (!CONVEX_URL) {
|
||||
console.error("NEXT_PUBLIC_CONVEX_URL não definido. Configure o endpoint do Convex e execute novamente.")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const TARGET_NAMES = new Set(["Ana Souza", "Bruno Lima"])
|
||||
const REPLACEMENT = {
|
||||
name: "Rever",
|
||||
email: "renan.pac@paulicon.com.br",
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const client = new ConvexHttpClient(CONVEX_URL)
|
||||
|
||||
const admin = await client.mutation("users:ensureUser", {
|
||||
tenantId: TENANT_ID,
|
||||
email: "admin@sistema.dev",
|
||||
name: "Administrador",
|
||||
role: "ADMIN",
|
||||
})
|
||||
|
||||
if (!admin?._id) {
|
||||
throw new Error("Não foi possível garantir o usuário administrador")
|
||||
}
|
||||
|
||||
const replacementUser = await client.mutation("users:ensureUser", {
|
||||
tenantId: TENANT_ID,
|
||||
email: REPLACEMENT.email,
|
||||
name: REPLACEMENT.name,
|
||||
role: "AGENT",
|
||||
})
|
||||
|
||||
if (!replacementUser?._id) {
|
||||
throw new Error("Não foi possível garantir o usuário Rever")
|
||||
}
|
||||
|
||||
const agents = await client.query("users:listAgents", { tenantId: TENANT_ID })
|
||||
const targets = agents.filter((agent) => TARGET_NAMES.has(agent.name))
|
||||
|
||||
if (targets.length === 0) {
|
||||
console.log("Nenhum responsável legado encontrado. Nada a atualizar.")
|
||||
}
|
||||
|
||||
const targetIds = new Set(targets.map((agent) => agent._id))
|
||||
|
||||
const tickets = await client.query("tickets:list", {
|
||||
tenantId: TENANT_ID,
|
||||
viewerId: admin._id,
|
||||
})
|
||||
|
||||
let reassignedCount = 0
|
||||
for (const ticket of tickets) {
|
||||
if (ticket.assignee && targetIds.has(ticket.assignee.id)) {
|
||||
await client.mutation("tickets:changeAssignee", {
|
||||
ticketId: ticket.id,
|
||||
assigneeId: replacementUser._id,
|
||||
actorId: admin._id,
|
||||
})
|
||||
reassignedCount += 1
|
||||
console.log(`Ticket ${ticket.reference} reatribuído para ${replacementUser.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const agent of targets) {
|
||||
try {
|
||||
await client.mutation("users:deleteUser", {
|
||||
userId: agent._id,
|
||||
actorId: admin._id,
|
||||
})
|
||||
console.log(`Usuário removido: ${agent.name}`)
|
||||
} catch (error) {
|
||||
console.error(`Falha ao remover ${agent.name}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Total de tickets reatribuídos: ${reassignedCount}`)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Erro ao reatribuir responsáveis legacy:", error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
139
scripts/seed-agents.mjs
Normal file
139
scripts/seed-agents.mjs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import "dotenv/config"
|
||||
import pkg from "@prisma/client"
|
||||
import { hashPassword } from "better-auth/crypto"
|
||||
import { ConvexHttpClient } from "convex/browser"
|
||||
|
||||
const { PrismaClient } = pkg
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const USERS = [
|
||||
{ name: "Administrador", email: "admin@sistema.dev", role: "admin" },
|
||||
{ name: "Gabriel Oliveira", email: "gabriel.oliveira@rever.com.br", role: "agent" },
|
||||
{ name: "George Araujo", email: "george.araujo@rever.com.br", role: "agent" },
|
||||
{ name: "Hugo Soares", email: "hugo.soares@rever.com.br", role: "agent" },
|
||||
{ name: "Julio Cesar", email: "julio@rever.com.br", role: "agent" },
|
||||
{ name: "Lorena Magalhães", email: "lorena@rever.com.br", role: "agent" },
|
||||
{ name: "Rever", email: "renan.pac@paulicon.com.br", role: "agent" },
|
||||
{ name: "Telão", email: "suporte@rever.com.br", role: "agent" },
|
||||
{ name: "Thiago Medeiros", email: "thiago.medeiros@rever.com.br", role: "agent" },
|
||||
{ name: "Weslei Magalhães", email: "weslei@rever.com.br", role: "agent" },
|
||||
]
|
||||
|
||||
const TENANT_ID = process.env.SEED_TENANT_ID ?? "tenant-atlas"
|
||||
const DEFAULT_AGENT_PASSWORD = process.env.SEED_AGENT_PASSWORD ?? "agent123"
|
||||
const DEFAULT_ADMIN_PASSWORD = process.env.SEED_ADMIN_PASSWORD ?? "admin123"
|
||||
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL
|
||||
|
||||
async function syncConvexUsers(users) {
|
||||
if (!CONVEX_URL) {
|
||||
console.warn("NEXT_PUBLIC_CONVEX_URL não definido; sincronização com Convex ignorada.")
|
||||
return
|
||||
}
|
||||
|
||||
const client = new ConvexHttpClient(CONVEX_URL)
|
||||
for (const user of users) {
|
||||
try {
|
||||
await client.mutation("users:ensureUser", {
|
||||
tenantId: TENANT_ID,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role.toUpperCase(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Falha ao sincronizar usuário ${user.email} com Convex`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const emails = USERS.map((user) => user.email.toLowerCase())
|
||||
|
||||
const existing = await prisma.authUser.findMany({
|
||||
where: {
|
||||
email: {
|
||||
notIn: emails,
|
||||
},
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
if (existing.length > 0) {
|
||||
const ids = existing.map((user) => user.id)
|
||||
await prisma.authSession.deleteMany({ where: { userId: { in: ids } } })
|
||||
await prisma.authAccount.deleteMany({ where: { userId: { in: ids } } })
|
||||
await prisma.authUser.deleteMany({ where: { id: { in: ids } } })
|
||||
}
|
||||
|
||||
const seededUsers = []
|
||||
|
||||
for (const definition of USERS) {
|
||||
const email = definition.email.toLowerCase()
|
||||
const role = definition.role ?? "agent"
|
||||
const password = definition.password ?? (role === "admin" ? DEFAULT_ADMIN_PASSWORD : DEFAULT_AGENT_PASSWORD)
|
||||
const hashedPassword = await hashPassword(password)
|
||||
|
||||
const user = await prisma.authUser.upsert({
|
||||
where: { email },
|
||||
update: {
|
||||
name: definition.name,
|
||||
role,
|
||||
tenantId: TENANT_ID,
|
||||
emailVerified: true,
|
||||
},
|
||||
create: {
|
||||
email,
|
||||
name: definition.name,
|
||||
role,
|
||||
tenantId: TENANT_ID,
|
||||
emailVerified: true,
|
||||
accounts: {
|
||||
create: {
|
||||
providerId: "credential",
|
||||
accountId: email,
|
||||
password: hashedPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { accounts: true },
|
||||
})
|
||||
|
||||
const credentialAccount = user.accounts.find(
|
||||
(account) => account.providerId === "credential" && account.accountId === email,
|
||||
)
|
||||
|
||||
if (credentialAccount) {
|
||||
await prisma.authAccount.update({
|
||||
where: { id: credentialAccount.id },
|
||||
data: { password: hashedPassword },
|
||||
})
|
||||
} else {
|
||||
await prisma.authAccount.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
providerId: "credential",
|
||||
accountId: email,
|
||||
password: hashedPassword,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
seededUsers.push({ id: user.id, name: definition.name, email, role })
|
||||
console.log(`✅ Usuário sincronizado: ${definition.name} <${email}> (${role})`)
|
||||
}
|
||||
|
||||
await syncConvexUsers(seededUsers)
|
||||
|
||||
console.log("")
|
||||
console.log(`Senha padrão agentes: ${DEFAULT_AGENT_PASSWORD}`)
|
||||
console.log(`Senha padrão administrador: ${DEFAULT_ADMIN_PASSWORD}`)
|
||||
console.log(`Total de usuários ativos: ${seededUsers.length}`)
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((error) => {
|
||||
console.error("Erro ao processar agentes", error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
220
scripts/seed-auth.mjs
Normal file
220
scripts/seed-auth.mjs
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import "dotenv/config"
|
||||
import pkg from "@prisma/client"
|
||||
import { hashPassword } from "better-auth/crypto"
|
||||
|
||||
const { PrismaClient } = pkg
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const tenantId = process.env.SEED_USER_TENANT ?? "tenant-atlas"
|
||||
|
||||
const singleUserFromEnv = process.env.SEED_USER_EMAIL
|
||||
? [{
|
||||
email: process.env.SEED_USER_EMAIL,
|
||||
password: process.env.SEED_USER_PASSWORD ?? "admin123",
|
||||
name: process.env.SEED_USER_NAME ?? "Administrador",
|
||||
role: process.env.SEED_USER_ROLE ?? "admin",
|
||||
tenantId,
|
||||
}]
|
||||
: null
|
||||
|
||||
const defaultUsers = singleUserFromEnv ?? [
|
||||
{
|
||||
email: "admin@sistema.dev",
|
||||
password: "admin123",
|
||||
name: "Administrador",
|
||||
role: "admin",
|
||||
tenantId,
|
||||
},
|
||||
{
|
||||
email: "cliente.demo@sistema.dev",
|
||||
password: "cliente123",
|
||||
name: "Cliente Demo",
|
||||
role: "customer",
|
||||
tenantId,
|
||||
},
|
||||
{
|
||||
email: "mariana.andrade@atlasengenharia.com.br",
|
||||
password: "manager123",
|
||||
name: "Mariana Andrade",
|
||||
role: "manager",
|
||||
tenantId,
|
||||
},
|
||||
{
|
||||
email: "fernanda.lima@omnisaude.com.br",
|
||||
password: "manager123",
|
||||
name: "Fernanda Lima",
|
||||
role: "manager",
|
||||
tenantId,
|
||||
},
|
||||
{
|
||||
email: "joao.ramos@atlasengenharia.com.br",
|
||||
password: "cliente123",
|
||||
name: "João Pedro Ramos",
|
||||
role: "customer",
|
||||
tenantId,
|
||||
},
|
||||
{
|
||||
email: "aline.rezende@atlasengenharia.com.br",
|
||||
password: "cliente123",
|
||||
name: "Aline Rezende",
|
||||
role: "customer",
|
||||
tenantId,
|
||||
},
|
||||
{
|
||||
email: "ricardo.matos@omnisaude.com.br",
|
||||
password: "cliente123",
|
||||
name: "Ricardo Matos",
|
||||
role: "customer",
|
||||
tenantId,
|
||||
},
|
||||
{
|
||||
email: "luciana.prado@omnisaude.com.br",
|
||||
password: "cliente123",
|
||||
name: "Luciana Prado",
|
||||
role: "customer",
|
||||
tenantId,
|
||||
},
|
||||
{
|
||||
email: "gabriel.oliveira@rever.com.br",
|
||||
password: "agent123",
|
||||
name: "Gabriel Oliveira",
|
||||
role: "agent",
|
||||
tenantId,
|
||||
},
|
||||
{
|
||||
email: "george.araujo@rever.com.br",
|
||||
password: "agent123",
|
||||
name: "George Araujo",
|
||||
role: "agent",
|
||||
tenantId,
|
||||
},
|
||||
{
|
||||
email: "hugo.soares@rever.com.br",
|
||||
password: "agent123",
|
||||
name: "Hugo Soares",
|
||||
role: "agent",
|
||||
tenantId,
|
||||
},
|
||||
{
|
||||
email: "julio@rever.com.br",
|
||||
password: "agent123",
|
||||
name: "Julio Cesar",
|
||||
role: "agent",
|
||||
tenantId,
|
||||
},
|
||||
{
|
||||
email: "lorena@rever.com.br",
|
||||
password: "agent123",
|
||||
name: "Lorena Magalhães",
|
||||
role: "agent",
|
||||
tenantId,
|
||||
},
|
||||
{
|
||||
email: "renan.pac@paulicon.com.br",
|
||||
password: "agent123",
|
||||
name: "Rever",
|
||||
role: "agent",
|
||||
tenantId,
|
||||
},
|
||||
{
|
||||
email: "thiago.medeiros@rever.com.br",
|
||||
password: "agent123",
|
||||
name: "Thiago Medeiros",
|
||||
role: "agent",
|
||||
tenantId,
|
||||
},
|
||||
{
|
||||
email: "weslei@rever.com.br",
|
||||
password: "agent123",
|
||||
name: "Weslei Magalhães",
|
||||
role: "agent",
|
||||
tenantId,
|
||||
},
|
||||
]
|
||||
|
||||
async function upsertAuthUser({ email, password, name, role, tenantId: userTenant }) {
|
||||
const hashedPassword = await hashPassword(password)
|
||||
|
||||
const user = await prisma.authUser.upsert({
|
||||
where: { email },
|
||||
update: {
|
||||
name,
|
||||
role,
|
||||
tenantId: userTenant,
|
||||
},
|
||||
create: {
|
||||
email,
|
||||
name,
|
||||
role,
|
||||
tenantId: userTenant,
|
||||
accounts: {
|
||||
create: {
|
||||
providerId: "credential",
|
||||
accountId: email,
|
||||
password: hashedPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
accounts: true,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.authAccount.updateMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
accountId: email,
|
||||
},
|
||||
data: {
|
||||
providerId: "credential",
|
||||
},
|
||||
})
|
||||
|
||||
let account = await prisma.authAccount.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
providerId: "credential",
|
||||
accountId: email,
|
||||
},
|
||||
})
|
||||
|
||||
if (account) {
|
||||
account = await prisma.authAccount.update({
|
||||
where: { id: account.id },
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
account = await prisma.authAccount.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
providerId: "credential",
|
||||
accountId: email,
|
||||
password: hashedPassword,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`✅ Usuario seed criado/atualizado: ${user.email}`)
|
||||
console.log(` ID: ${user.id}`)
|
||||
console.log(` Role: ${user.role}`)
|
||||
console.log(` Tenant: ${user.tenantId ?? "(nenhum)"}`)
|
||||
console.log(` Provider: ${account?.providerId ?? "-"}`)
|
||||
console.log(` Senha provisoria: ${password}`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
for (const user of defaultUsers) {
|
||||
await upsertAuthUser(user)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((error) => {
|
||||
console.error("Erro ao criar usuario seed", error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
156
scripts/sync-prisma-to-convex.mjs
Normal file
156
scripts/sync-prisma-to-convex.mjs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import "dotenv/config"
|
||||
import { PrismaClient } from "@prisma/client"
|
||||
import { ConvexHttpClient } from "convex/browser"
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
function toMillis(date) {
|
||||
return date instanceof Date ? date.getTime() : date ? new Date(date).getTime() : undefined
|
||||
}
|
||||
|
||||
function normalizeString(value, fallback = "") {
|
||||
if (!value) return fallback
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
function slugify(value) {
|
||||
return normalizeString(value)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-") || undefined
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const tenantId = process.env.SYNC_TENANT_ID || "tenant-atlas"
|
||||
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL || "http://127.0.0.1:3210"
|
||||
const secret = process.env.CONVEX_SYNC_SECRET
|
||||
|
||||
if (!secret) {
|
||||
console.error("CONVEX_SYNC_SECRET não configurado. Configure no .env.")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const [users, queues, tickets, companies] = await Promise.all([
|
||||
prisma.user.findMany({
|
||||
include: {
|
||||
teams: {
|
||||
include: { team: true },
|
||||
},
|
||||
company: true,
|
||||
},
|
||||
}),
|
||||
prisma.queue.findMany(),
|
||||
prisma.ticket.findMany({
|
||||
include: {
|
||||
requester: true,
|
||||
assignee: true,
|
||||
queue: true,
|
||||
company: true,
|
||||
comments: {
|
||||
include: {
|
||||
author: true,
|
||||
},
|
||||
},
|
||||
events: true,
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
}),
|
||||
prisma.company.findMany(),
|
||||
])
|
||||
|
||||
const userSnapshot = users.map((user) => ({
|
||||
email: user.email,
|
||||
name: normalizeString(user.name, user.email),
|
||||
role: user.role,
|
||||
avatarUrl: user.avatarUrl ?? undefined,
|
||||
teams: user.teams
|
||||
.map((membership) => membership.team?.name)
|
||||
.filter((name) => Boolean(name) && typeof name === "string"),
|
||||
companySlug: user.company?.slug ?? undefined,
|
||||
}))
|
||||
|
||||
const queueSnapshot = queues.map((queue) => ({
|
||||
name: normalizeString(queue.name, queue.slug ?? queue.id),
|
||||
slug: queue.slug ? queue.slug : normalizeString(queue.name, queue.id).toLowerCase().replace(/\s+/g, "-"),
|
||||
}))
|
||||
|
||||
const referenceFallbackStart = 41000
|
||||
let referenceCounter = referenceFallbackStart
|
||||
|
||||
const ticketSnapshot = tickets.map((ticket) => {
|
||||
const reference = ticket.reference && ticket.reference > 0 ? ticket.reference : ++referenceCounter
|
||||
const requesterEmail = ticket.requester?.email ?? userSnapshot[0]?.email ?? "unknown@example.com"
|
||||
const assigneeEmail = ticket.assignee?.email ?? undefined
|
||||
const queueSlug = ticket.queue?.slug ?? slugify(ticket.queue?.name)
|
||||
const companySlug = ticket.company?.slug ?? ticket.requester?.company?.slug ?? undefined
|
||||
|
||||
return {
|
||||
reference,
|
||||
subject: normalizeString(ticket.subject, `Ticket ${reference}`),
|
||||
summary: ticket.summary ?? undefined,
|
||||
status: ticket.status,
|
||||
priority: ticket.priority,
|
||||
channel: ticket.channel,
|
||||
queueSlug: queueSlug ?? undefined,
|
||||
requesterEmail,
|
||||
assigneeEmail,
|
||||
companySlug,
|
||||
dueAt: toMillis(ticket.dueAt) ?? undefined,
|
||||
firstResponseAt: toMillis(ticket.firstResponseAt) ?? undefined,
|
||||
resolvedAt: toMillis(ticket.resolvedAt) ?? undefined,
|
||||
closedAt: toMillis(ticket.closedAt) ?? undefined,
|
||||
createdAt: toMillis(ticket.createdAt) ?? Date.now(),
|
||||
updatedAt: toMillis(ticket.updatedAt) ?? Date.now(),
|
||||
tags: Array.isArray(ticket.tags) ? ticket.tags : undefined,
|
||||
comments: ticket.comments.map((comment) => ({
|
||||
authorEmail: comment.author?.email ?? requesterEmail,
|
||||
visibility: comment.visibility,
|
||||
body: comment.body,
|
||||
createdAt: toMillis(comment.createdAt) ?? Date.now(),
|
||||
updatedAt: toMillis(comment.updatedAt) ?? Date.now(),
|
||||
})),
|
||||
events: ticket.events.map((event) => ({
|
||||
type: event.type,
|
||||
payload: event.payload ?? {},
|
||||
createdAt: toMillis(event.createdAt) ?? Date.now(),
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
const companySnapshot = companies.map((company) => ({
|
||||
slug: company.slug ?? slugify(company.name),
|
||||
name: company.name,
|
||||
cnpj: company.cnpj ?? undefined,
|
||||
domain: company.domain ?? undefined,
|
||||
phone: company.phone ?? undefined,
|
||||
description: company.description ?? undefined,
|
||||
address: company.address ?? undefined,
|
||||
createdAt: toMillis(company.createdAt) ?? Date.now(),
|
||||
updatedAt: toMillis(company.updatedAt) ?? Date.now(),
|
||||
}))
|
||||
|
||||
const client = new ConvexHttpClient(convexUrl)
|
||||
|
||||
const result = await client.mutation("migrations:importPrismaSnapshot", {
|
||||
secret,
|
||||
snapshot: {
|
||||
tenantId,
|
||||
companies: companySnapshot,
|
||||
users: userSnapshot,
|
||||
queues: queueSnapshot,
|
||||
tickets: ticketSnapshot,
|
||||
},
|
||||
})
|
||||
|
||||
console.log("Sincronização concluída:", result)
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue