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

47
scripts/debug-convex.mjs Normal file
View 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);

View 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)
})

View 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()
})

View 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
View 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
View 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()
})

View 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()
})