Files
agenciapsilmno/scripts/simulation/simulateUsage.js
2026-03-12 08:58:36 -03:00

556 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* simulateUsage.js
*
* Gera dois arquivos SQL:
* logs/simulation-seed.sql → insere dados de simulação
* logs/simulation-cleanup.sql → remove os dados inseridos
*
* e um relatório legível:
* logs/simulation-report.txt
* logs/simulation-log.txt
*
* COMO USAR:
* 1. Preencha scripts/simulation/simulation.config.js com seu OWNER_ID e TENANT_ID
* 2. npm run simulate
* 3. Abra o Supabase SQL Editor e rode simulation-seed.sql
* 4. Teste o sistema
* 5. Quando terminar, rode simulation-cleanup.sql para limpar
*/
import fs from 'fs'
import path from 'path'
import { config } from './simulation.config.js'
import { logInfo, logWarning, logError, getLog } from './simulationLogger.js'
// ─── Validação ────────────────────────────────────────────────────────────────
if (config.OWNER_ID === 'SEU-OWNER-UUID-AQUI' || config.TENANT_ID === 'SEU-TENANT-UUID-AQUI') {
console.error('\n❌ ERRO: Preencha OWNER_ID e TENANT_ID em scripts/simulation/simulation.config.js\n')
process.exit(1)
}
// ─── Dados brasileiros falsos ─────────────────────────────────────────────────
const NOMES = [
['Ana', 'Beatriz', 'Carla', 'Daniela', 'Fernanda', 'Gabriela', 'Helena', 'Isabela',
'Juliana', 'Karen', 'Laura', 'Mariana', 'Natália', 'Olivia', 'Patrícia'],
['Carlos', 'Daniel', 'Eduardo', 'Felipe', 'Gustavo', 'Henrique', 'Igor', 'João',
'Lucas', 'Marcos', 'Nelson', 'Otávio', 'Paulo', 'Rafael', 'Sérgio'],
]
const SOBRENOMES = [
'Silva', 'Santos', 'Oliveira', 'Souza', 'Lima', 'Ferreira', 'Rodrigues',
'Almeida', 'Costa', 'Gomes', 'Martins', 'Pereira', 'Carvalho', 'Rocha', 'Nunes',
]
const MODALIDADES = ['presencial', 'online', 'presencial', 'presencial'] // ponderado
// ─── Utilitários ─────────────────────────────────────────────────────────────
let _rng = 1
function rng () {
// LCG determinístico para gerar sequência reproduzível
_rng = (_rng * 1664525 + 1013904223) & 0xffffffff
return Math.abs(_rng) / 0x80000000
}
function pick (arr) { return arr[Math.floor(rng() * arr.length)] }
function uuid () {
// gera UUID v4-like determinístico baseado no nosso rng
const h = () => Math.floor(rng() * 0x10000).toString(16).padStart(4, '0')
return `${h()}${h()}-${h()}-4${h().slice(1)}-${(8 + Math.floor(rng() * 4)).toString(16)}${h().slice(1)}-${h()}${h()}${h()}`
}
function addDays (date, n) {
const d = new Date(date)
d.setDate(d.getDate() + n)
return d
}
function toISO (date) {
return date.toISOString().split('T')[0]
}
function toISODateTime (date, hh, mm) {
return `${toISO(date)}T${String(hh).padStart(2,'0')}:${String(mm).padStart(2,'0')}:00`
}
function sqlStr (v) {
if (v === null || v === undefined) return 'NULL'
return `'${String(v).replace(/'/g, "''")}'`
}
function weekdayName (n) {
return ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'][n]
}
// ─── Gerador de nomes únicos ──────────────────────────────────────────────────
const usedNames = new Set()
function fakeName () {
let attempts = 0
while (attempts < 50) {
const gender = rng() > 0.5 ? 0 : 1
const nome = pick(NOMES[gender])
const sobrenome = pick(SOBRENOMES)
const full = `${nome} ${sobrenome}`
if (!usedNames.has(full)) {
usedNames.add(full)
return full
}
attempts++
}
return `Paciente ${usedNames.size + 1}`
}
function fakeCpf () {
const n = () => Math.floor(rng() * 9) + 1
return `${n()}${n()}${n()}${n()}${n()}${n()}${n()}${n()}${n()}${n()}${n()}`
}
function fakePhone () {
const ddd = [11,21,31,41,51,61,71,81,91][Math.floor(rng() * 9)]
const num = Math.floor(rng() * 900000000) + 900000000
return `(${ddd}) 9${String(num).slice(1,5)}-${String(num).slice(5,9)}`
}
function fakeEmail (nome) {
const slug = nome.toLowerCase()
.replace(/\s+/g, '.')
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
const num = Math.floor(rng() * 99) + 1
const domains = ['gmail.com','hotmail.com','outlook.com','yahoo.com.br']
return `${slug}${num}@${pick(domains)}`
}
// ─── Blocos SQL ───────────────────────────────────────────────────────────────
const seedLines = []
const cleanupIds = {
patients: [],
agendaEventos: [],
recurrenceRules: [],
recurrenceExceptions: [],
agendadorSolicitacoes: [],
agendaConfiguracoes: [],
agendaRegrasSemanais: [],
}
function emit (line) { seedLines.push(line) }
// ─── Construção da simulação ──────────────────────────────────────────────────
const TODAY = new Date()
TODAY.setHours(0, 0, 0, 0)
const PAST_START = addDays(TODAY, -config.DAYS_BACK)
const FUTURE_END = addDays(TODAY, config.SIMULATION_DAYS)
const ownerId = config.OWNER_ID
const tenantId = config.TENANT_ID
logInfo('Iniciando simulação...')
logInfo(`OWNER_ID: ${ownerId}`)
logInfo(`TENANT_ID: ${tenantId}`)
logInfo(`Período: ${toISO(PAST_START)}${toISO(FUTURE_END)}`)
// ─── 1. Pacientes ─────────────────────────────────────────────────────────────
emit('-- ============================================================')
emit('-- SIMULAÇÃO AgenciaPsi — gerado por npm run simulate')
emit(`-- Data: ${new Date().toLocaleString('pt-BR')}`)
emit('-- ============================================================')
emit('')
emit('-- Desabilita triggers para permitir inserts de simulação')
emit('SET session_replication_role = replica;')
emit('')
emit('BEGIN;')
emit('')
emit('-- ─── 1. Pacientes ───────────────────────────────────────────────────────────')
emit('')
const patients = []
for (let i = 0; i < config.PATIENTS_COUNT; i++) {
const id = uuid()
const nome = fakeName()
const parts = nome.split(' ')
const email = fakeEmail(nome)
const tel = fakePhone()
const cpf = fakeCpf()
patients.push({ id, nome_completo: nome, email, telefone: tel })
cleanupIds.patients.push(id)
emit(`INSERT INTO patients (id, tenant_id, owner_id, responsible_member_id, nome_completo, email_principal, telefone, cpf, patient_scope, status)`)
emit(` VALUES (${sqlStr(id)}, ${sqlStr(tenantId)}, ${sqlStr(ownerId)},`)
emit(` (SELECT id FROM tenant_members WHERE user_id = ${sqlStr(ownerId)} AND tenant_id = ${sqlStr(tenantId)} LIMIT 1),`)
emit(` ${sqlStr(nome)}, ${sqlStr(email)}, ${sqlStr(tel)}, ${sqlStr(cpf)}, 'clinic', 'Ativo');`)
}
logInfo(`${patients.length} pacientes criados`)
emit('')
// ─── 2. Configurações de agenda ───────────────────────────────────────────────
emit('-- ─── 2. Configurações de agenda ─────────────────────────────────────────────')
emit('')
const pausas = [
{ weekday: 1, start: '12:00', end: '13:00', label: 'Almoço' },
{ weekday: 2, start: '12:00', end: '13:00', label: 'Almoço' },
{ weekday: 3, start: '12:00', end: '13:00', label: 'Almoço' },
{ weekday: 4, start: '12:00', end: '13:00', label: 'Almoço' },
{ weekday: 5, start: '12:00', end: '13:00', label: 'Almoço' },
]
emit(`INSERT INTO agenda_configuracoes (owner_id, tenant_id, session_duration_min, session_break_min, pausas_semanais, online_ativo, setup_clinica_concluido)`)
emit(` VALUES (${sqlStr(ownerId)}, ${sqlStr(tenantId)},`)
emit(` ${config.SESSION_DURATION_MIN}, 10, '${JSON.stringify(pausas)}'::jsonb, true, true)`)
emit(` ON CONFLICT (owner_id) DO NOTHING;`)
emit('')
logInfo('✔ agenda_configuracoes inserida')
// ─── 3. Regras semanais de disponibilidade ────────────────────────────────────
emit('-- ─── 3. Regras semanais ─────────────────────────────────────────────────────')
emit('')
const workDays = [1, 2, 3, 4, 5] // segsex
for (const dia of workDays) {
const rid = uuid()
cleanupIds.agendaRegrasSemanais.push(rid)
emit(`INSERT INTO agenda_regras_semanais (id, owner_id, tenant_id, dia_semana, hora_inicio, hora_fim, ativo)`)
emit(` VALUES (${sqlStr(rid)}, ${sqlStr(ownerId)}, ${sqlStr(tenantId)}, ${dia}, '08:00', '18:00', true);`)
}
logInfo(`${workDays.length} regras semanais inseridas`)
emit('')
// ─── 4. Eventos avulsos (passado) ─────────────────────────────────────────────
emit('-- ─── 4. Eventos avulsos (passado) ──────────────────────────────────────────')
emit('')
const avulsoCount = 3
const avulsoPatients = patients.slice(0, avulsoCount)
let avulsosCreated = 0
// Avulsos usam 13h para não conflitar com séries (9h12h e 14h17h)
for (const pat of avulsoPatients) {
const daysAgo = Math.floor(rng() * config.DAYS_BACK) + 1
const date = addDays(TODAY, -daysAgo)
if (date.getDay() === 0 || date.getDay() === 6) continue // pular fds
const hour = 13
const startDT = toISODateTime(date, hour, 0)
const endDT = toISODateTime(date, hour, 50)
const evId = uuid()
const modal = pick(MODALIDADES)
const statuses = ['realizado', 'faltou', 'agendado']
const status = pick(statuses)
cleanupIds.agendaEventos.push(evId)
emit(`INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, titulo)`)
emit(` VALUES (${sqlStr(evId)}, ${sqlStr(ownerId)}, ${sqlStr(tenantId)}, ${sqlStr(pat.id)},`)
emit(` 'sessao', ${sqlStr(status)}, ${sqlStr(startDT)}, ${sqlStr(endDT)}, ${sqlStr(modal)}, 'Sessão avulsa');`)
avulsosCreated++
}
logInfo(`${avulsosCreated} eventos avulsos criados`)
emit('')
// ─── 5. Séries de recorrência ─────────────────────────────────────────────────
emit('-- ─── 5. Séries de recorrência ───────────────────────────────────────────────')
emit('')
const ruleStartDate = addDays(TODAY, -config.DAYS_BACK)
const ruleEndDate = addDays(TODAY, config.SIMULATION_DAYS)
// Distribui pacientes pelas séries (cicla se menos pacientes que séries)
const seriesPatients = patients.slice(0, config.RECURRENCE_RULES_COUNT)
const types = ['weekly', 'weekly', 'weekly', 'biweekly', 'custom_weekdays', 'weekly']
const wdays = [[1], [2], [4], [1], [1,3], [3]]
// Cada série tem seu próprio horário para evitar sobreposição
const ruleHours = [9, 10, 11, 14, 15, 16]
const recurrenceRules = []
for (let i = 0; i < config.RECURRENCE_RULES_COUNT; i++) {
const pat = seriesPatients[i % seriesPatients.length]
const type = types[i] || 'weekly'
const weekdays = wdays[i] || [1]
const modal = pick(MODALIDADES)
const ruleId = uuid()
const hour = ruleHours[i] || (9 + i)
const startTime = `${String(hour).padStart(2,'0')}:00`
const endTime = `${String(hour).padStart(2,'0')}:50`
const weekdaysArr = `ARRAY[${weekdays.join(',')}]::smallint[]`
recurrenceRules.push({ id: ruleId, patient: pat, type, weekdays, start_date: ruleStartDate, modal, hour })
cleanupIds.recurrenceRules.push(ruleId)
emit(`INSERT INTO recurrence_rules (id, owner_id, tenant_id, patient_id, type, weekdays, interval, start_date, end_date, status, start_time, end_time, modalidade)`)
emit(` VALUES (${sqlStr(ruleId)}, ${sqlStr(ownerId)}, ${sqlStr(tenantId)}, ${sqlStr(pat.id)},`)
emit(` ${sqlStr(type)}, ${weekdaysArr}, 1, ${sqlStr(toISO(ruleStartDate))}, ${sqlStr(toISO(ruleEndDate))},`)
emit(` 'ativo', ${sqlStr(startTime)}, ${sqlStr(endTime)}, ${sqlStr(modal)});`)
}
logInfo(`${recurrenceRules.length} regras de recorrência criadas`)
emit('')
// ─── 6. Exceções de recorrência (passado) ────────────────────────────────────
emit('-- ─── 6. Exceções de recorrência ────────────────────────────────────────────')
emit('')
let excFaltou = 0, excRemarcado = 0, excCancelado = 0
for (const rr of recurrenceRules) {
// Gera datas das ocorrências passadas
const occs = []
let cur = new Date(rr.start_date)
// avançar para o primeiro dia correto
while (cur <= TODAY) {
const dow = cur.getDay()
if (rr.weekdays.includes(dow)) {
if (cur < TODAY) occs.push(new Date(cur))
}
cur = addDays(cur, 1)
}
for (const occDate of occs) {
const r = rng()
const dateStr = toISO(occDate)
if (r < config.RATE_FALTOU) {
const excId = uuid()
cleanupIds.recurrenceExceptions.push(excId)
emit(`INSERT INTO recurrence_exceptions (id, recurrence_id, tenant_id, original_date, type, new_date)`)
emit(` VALUES (${sqlStr(excId)}, ${sqlStr(rr.id)}, ${sqlStr(tenantId)}, ${sqlStr(dateStr)}, 'patient_missed', NULL);`)
excFaltou++
} else if (r < config.RATE_FALTOU + config.RATE_REMARCADO) {
const offset = Math.floor(rng() * 4) + 1
const newDate = addDays(occDate, offset)
// não remarcar para fds
if (newDate.getDay() === 0 || newDate.getDay() === 6) continue
const excId = uuid()
cleanupIds.recurrenceExceptions.push(excId)
emit(`INSERT INTO recurrence_exceptions (id, recurrence_id, tenant_id, original_date, type, new_date)`)
emit(` VALUES (${sqlStr(excId)}, ${sqlStr(rr.id)}, ${sqlStr(tenantId)}, ${sqlStr(dateStr)}, 'reschedule_session', ${sqlStr(toISO(newDate))});`)
excRemarcado++
} else if (r < config.RATE_FALTOU + config.RATE_REMARCADO + config.RATE_CANCELADO) {
const excId = uuid()
cleanupIds.recurrenceExceptions.push(excId)
emit(`INSERT INTO recurrence_exceptions (id, recurrence_id, tenant_id, original_date, type, new_date)`)
emit(` VALUES (${sqlStr(excId)}, ${sqlStr(rr.id)}, ${sqlStr(tenantId)}, ${sqlStr(dateStr)}, 'cancel_session', NULL);`)
excCancelado++
}
}
}
logInfo(`✔ Exceções: ${excFaltou} faltou, ${excRemarcado} remarcado, ${excCancelado} cancelado`)
emit('')
// ─── 7. Sessões reais para ocorrências passadas (realizado/faltou) ────────────
emit('-- ─── 7. Sessões reais (passado — realizado/faltou) ────────────────────────')
emit('')
let realSessionsCreated = 0
for (const rr of recurrenceRules) {
let cur2 = new Date(rr.start_date)
while (cur2 < TODAY) {
const dow = cur2.getDay()
if (rr.weekdays.includes(dow)) {
const dateStr = toISO(cur2)
const evId = uuid()
const hh = String(rr.hour).padStart(2, '0')
const startDT = `${dateStr}T${hh}:00:00`
const endDT = `${dateStr}T${hh}:50:00`
// status baseado nas exceções: se há exceção faltou → faltou, else → realizado
// simplificado: 80% realizado, 20% faltou para sessões passadas
const status = rng() < 0.8 ? 'realizado' : 'faltou'
cleanupIds.agendaEventos.push(evId)
emit(`INSERT INTO agenda_eventos (id, owner_id, tenant_id, patient_id, tipo, status, inicio_em, fim_em, modalidade, recurrence_id, recurrence_date)`)
emit(` VALUES (${sqlStr(evId)}, ${sqlStr(ownerId)}, ${sqlStr(tenantId)}, ${sqlStr(rr.patient.id)},`)
emit(` 'sessao', ${sqlStr(status)}, ${sqlStr(startDT)}, ${sqlStr(endDT)}, ${sqlStr(rr.modal)}, ${sqlStr(rr.id)}, ${sqlStr(dateStr)});`)
realSessionsCreated++
}
cur2 = addDays(cur2, 1)
}
}
logInfo(`${realSessionsCreated} sessões reais (passado) criadas`)
emit('')
// ─── 8. Solicitações do Agendador Público ─────────────────────────────────────
emit('-- ─── 8. Agendador Público — solicitações pendentes ─────────────────────────')
emit('')
const agendadorStatuses = ['pendente', 'pendente', 'pendente', 'autorizado', 'recusado']
for (let i = 0; i < config.AGENDADOR_REQUESTS_COUNT; i++) {
const nome = fakeName()
const parts2 = nome.split(' ')
const primeiro = parts2[0]
const sobrenome = parts2.slice(1).join(' ')
const email = fakeEmail(nome)
const cel = fakePhone()
const daysAhead = Math.floor(rng() * 14) + 1
const reqDate = addDays(TODAY, daysAhead)
if (reqDate.getDay() === 0 || reqDate.getDay() === 6) continue
const hour = config.WORK_HOUR_START + Math.floor(rng() * 6)
const hora = `${String(hour).padStart(2,'0')}:00`
const modal = pick(MODALIDADES)
const status = pick(agendadorStatuses)
const solId = uuid()
cleanupIds.agendadorSolicitacoes.push(solId)
emit(`INSERT INTO agendador_solicitacoes (id, owner_id, tenant_id, paciente_nome, paciente_sobrenome, paciente_email, paciente_celular, tipo, modalidade, data_solicitada, hora_solicitada, status)`)
emit(` VALUES (${sqlStr(solId)}, ${sqlStr(ownerId)}, ${sqlStr(tenantId)}, ${sqlStr(primeiro)}, ${sqlStr(sobrenome)}, ${sqlStr(email)}, ${sqlStr(cel)},`)
emit(` ${sqlStr(pick(['primeira', 'retorno', 'reagendar']))}, ${sqlStr(modal)}, ${sqlStr(toISO(reqDate))}, ${sqlStr(hora)}, ${sqlStr(status)});`)
}
logInfo(`${config.AGENDADOR_REQUESTS_COUNT} solicitações do agendador criadas`)
emit('')
// ─── 9. Fechar transação ──────────────────────────────────────────────────────
emit('COMMIT;')
emit('')
emit('-- Restaura comportamento normal dos triggers')
emit('SET session_replication_role = DEFAULT;')
emit('')
emit('-- ─── Fim do seed ────────────────────────────────────────────────────────────')
// ─── Gerar SQL de cleanup ────────────────────────────────────────────────────
function buildCleanupSQL () {
const lines = []
lines.push('-- ============================================================')
lines.push('-- CLEANUP — remove dados da simulação AgenciaPsi')
lines.push(`-- Data: ${new Date().toLocaleString('pt-BR')}`)
lines.push('-- ============================================================')
lines.push('')
lines.push('SET session_replication_role = replica;')
lines.push('')
lines.push('BEGIN;')
lines.push('')
function delBlock (table, ids) {
if (ids.length === 0) return
const quoted = ids.map(id => `'${id}'`).join(', ')
lines.push(`DELETE FROM ${table} WHERE id IN (${quoted});`)
}
delBlock('recurrence_exceptions', cleanupIds.recurrenceExceptions)
lines.push('')
delBlock('agenda_eventos', cleanupIds.agendaEventos)
lines.push('')
delBlock('recurrence_rules', cleanupIds.recurrenceRules)
lines.push('')
delBlock('agendador_solicitacoes', cleanupIds.agendadorSolicitacoes)
lines.push('')
delBlock('agenda_regras_semanais', cleanupIds.agendaRegrasSemanais)
lines.push('')
// agenda_configuracoes: PK é owner_id, não id
lines.push(`-- Descomente se quiser remover também as configurações de agenda:`)
lines.push(`-- DELETE FROM agenda_configuracoes WHERE owner_id = ${sqlStr(ownerId)};`)
lines.push('')
delBlock('patients', cleanupIds.patients)
lines.push('')
lines.push('COMMIT;')
lines.push('')
lines.push('SET session_replication_role = DEFAULT;')
lines.push('')
lines.push('-- ─── Fim do cleanup ─────────────────────────────────────────────────────────')
return lines.join('\n')
}
// ─── Relatório ────────────────────────────────────────────────────────────────
function buildReport () {
const lines = []
lines.push('============================================================')
lines.push(' RELATÓRIO DE SIMULAÇÃO — AgenciaPsi')
lines.push(` Gerado em: ${new Date().toLocaleString('pt-BR')}`)
lines.push('============================================================')
lines.push('')
lines.push(`OWNER_ID: ${ownerId}`)
lines.push(`TENANT_ID: ${tenantId}`)
lines.push('')
lines.push('─── Dados gerados ─────────────────────────────────────────')
lines.push(` Pacientes: ${patients.length}`)
lines.push(` Séries de recorrência: ${recurrenceRules.length}`)
lines.push(` Sessões reais (passado): ${realSessionsCreated}`)
lines.push(` Eventos avulsos: ${avulsosCreated}`)
lines.push(` Exceções — faltou: ${excFaltou}`)
lines.push(` Exceções — remarcado: ${excRemarcado}`)
lines.push(` Exceções — cancelado: ${excCancelado}`)
lines.push(` Solicitações agendador: ${config.AGENDADOR_REQUESTS_COUNT}`)
lines.push('')
lines.push('─── Pacientes criados ─────────────────────────────────────')
for (const p of patients) {
lines.push(` [${p.id}] ${p.nome_completo}${p.email}`)
}
lines.push('')
lines.push('─── Séries de recorrência ─────────────────────────────────')
for (const r of recurrenceRules) {
const dias = r.weekdays.map(d => weekdayName(d)).join(', ')
lines.push(` [${r.id}]`)
lines.push(` Paciente: ${r.patient.nome_completo}`)
lines.push(` Tipo: ${r.type} | Dias: ${dias}`)
lines.push(` Período: ${toISO(r.start_date)}${toISO(FUTURE_END)}`)
}
lines.push('')
lines.push('─── Como testar ───────────────────────────────────────────')
lines.push(' 1. Abra o Supabase SQL Editor')
lines.push(' 2. Cole e rode: logs/simulation-seed.sql')
lines.push(' 3. Acesse a agenda — os eventos devem aparecer')
lines.push(' 4. Acesse Pacientes — pacientes simulados aparecem na lista')
lines.push(' 5. Acesse Agendamentos Recebidos — solicitações pendentes')
lines.push(' 6. Quando terminar, rode: logs/simulation-cleanup.sql')
lines.push('')
lines.push('============================================================')
return lines.join('\n')
}
// ─── Salvar arquivos ─────────────────────────────────────────────────────────
const outDir = path.resolve(config.OUTPUT_DIR)
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true })
const seedPath = path.join(outDir, 'simulation-seed.sql')
const cleanupPath = path.join(outDir, 'simulation-cleanup.sql')
const reportPath = path.join(outDir, 'simulation-report.txt')
const logPath = path.join(outDir, 'simulation-log.txt')
fs.writeFileSync(seedPath, seedLines.join('\n'), 'utf-8')
fs.writeFileSync(cleanupPath, buildCleanupSQL(), 'utf-8')
fs.writeFileSync(reportPath, buildReport(), 'utf-8')
logInfo(`✔ Seed SQL: ${seedPath}`)
logInfo(`✔ Cleanup SQL: ${cleanupPath}`)
logInfo(`✔ Relatório: ${reportPath}`)
fs.writeFileSync(logPath, getLog(), 'utf-8')
console.log('\n✅ Simulação concluída. Arquivos em logs/')
console.log(' → Rode simulation-seed.sql no Supabase SQL Editor para inserir os dados')
console.log(' → Rode simulation-cleanup.sql quando quiser remover\n')