/** * 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]; // seg–sex 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 (9h–12h e 14h–17h) 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');