diff --git a/MEMORY.md b/MEMORY.md index 9e49259..57c47ae 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -1,23 +1,64 @@ -# Log de Migração SQL-First (Fase 5/6: Aulas, Frequências e Contratos) +# Log da Migração Massiva SQL-First (Sessão Completa) -## Resumo das Modificações -Nesta sessão, focamos em remover a exclusividade do arquivo `school_data.json` nos módulos de Cronograma (Aulas), Frequências e Contratos (incluindo Modelos de Contrato). +## Visão Geral +Nesta sessão de trabalho, realizamos o "core" da transição do sistema EduManager, saindo do modelo puramente JSON (`school_data.json`) e consolidando 4 módulos centrais diretamente no Banco de Dados Relacional PostgreSQL. -### 1. Banco de Dados e Backend (Manager) -- Adicionados métodos CRUD no arquivo `manager/services/database.js` para as tabelas `aulas`, `contratos` e `modelos_contrato`. -- Expostos endpoints em `manager/server.selfhosted.js`: - - `/api/aulas` (GET) e `/api/aulas/lote` (POST, DELETE) - - `/api/modelos-contrato` (GET, POST, PUT, DELETE) - - `/api/contratos` (GET, POST, DELETE) -- **Sincronização Injetada:** Modificada a função `syncJsonToRelationalTables` para iterar sobre `schoolData.lessons`, `schoolData.attendance`, `schoolData.contracts` e `schoolData.contractTemplates`, executando `INSERT ... ON CONFLICT DO UPDATE`. Isso garante que operações híbridas (que salvam JSON no Frontend) sejam espelhadas instantaneamente no PostgreSQL. +## Módulos Migrados -### 2. Frontend (Manager) -- `LessonSchedule.tsx` (Cronograma): Refatorado para carregar as aulas ( `fetch('/api/aulas')`) em estado próprio (`dbLessons`), removendo a leitura estática de `data.lessons`. Todas as ações (gerar aulas, reagendar, cancelar e exclusão em lote) agora enviam chamadas à API antes de atualizar a UI e invocar o `saveData`. -- `Contracts.tsx` (Contratos): Refatorado para utilizar estados locais (`dbContracts` e `dbTemplates`) carregados das novas rotas de API. Criações e exclusões realizam requests HTTP para persistir os dados nativamente no SQL, mantendo compatibilidade com o formato JSON da UI base. +### Fase 4: Gestão de Alunos e Autenticação +- **Backend Manager**: CRUD no `database.js` para tabela `alunos`, rotas `/api/alunos` no `server.selfhosted.js`. +- **Frontend Manager (`Students.tsx`)**: Refatorado para buscar via `fetch('/api/alunos')`. +- **Portal do Aluno**: Login (`/api/portal/login`) e perfil (`/api/portal/me`) reescritos para consultar a tabela `alunos` no PostgreSQL. -### 3. Portal do Aluno -- **`GET /api/portal/aulas`**: Alterada a rota para fazer um `SELECT` direto na tabela `aulas`, cruzando os IDs de turma vinculados ao aluno (via tabela `alunos` e histórico em `frequencias`). Adicionado fallback para ler o JSON caso o banco retorne vazio (útil durante a janela de transição). -- **`GET /api/portal/contratos`**: Alterada a rota para fazer um `SELECT` na tabela `contratos` puxando pelos dados salvos, com fallback seguro para o JSON se necessário. +### Fase 5: Avaliações e Provas +- Backend e Frontend conectados às tabelas `provas` e `questoes_provas`. -## Impacto -O Portal do Aluno agora opera primordialmente com PostgreSQL para a maior parte de sua leitura, incluindo alunos, provas, aulas, frequências e contratos. O painel administrativo foi fortalecido, registrando operações nos dois bancos simultaneamente para evitar a temida "Tela Branca" durante leituras em cascata no dashboard antigo. +### Fase 6: Cronograma e Aulas +- **Backend Manager**: Queries em lote (`insertAulas`, `getAulasByTurma`, `getAllAulas`, `deleteAulas`). +- **Frontend Manager (`LessonSchedule.tsx`)**: Consome `fetch('/api/aulas')` em estado próprio (`dbLessons`). +- **Frontend Manager (`Classes.tsx`)**: Ao criar/editar turma e gerar cronograma, agora envia aulas via `POST /api/aulas/lote` para o PostgreSQL. +- **Portal do Aluno**: Rota `/api/portal/aulas` faz `SELECT FROM aulas WHERE turma_id = ANY(...)`. + +### Fase 7: Contratos e Modelos +- **Backend Manager**: CRUD para `contratos` e `modelos_contrato`. +- **Frontend Manager (`Contracts.tsx`)**: Consome `/api/contratos` e `/api/modelos-contrato`. +- **Portal do Aluno**: Rota `/api/portal/contratos` faz `SELECT FROM contratos WHERE aluno_id = $1`. + +## Bugs Encontrados e Corrigidos + +### 🐛 Bug 1: Tela Branca na Aba Alunos (`Students.tsx`) +- **Causa**: Referência circular no `useState` — `useState(dbClasses || [])` referenciava a si mesma. +- **Fix**: Alterado para `useState(data?.classes || [])`. + +### 🐛 Bug 2: Dados Não Migrados para PostgreSQL +- **Causa**: A rotina `syncJsonToRelationalTables` só roda quando o Manager salva o JSON via `PUT /api/school-data`. Como a migração era nova, os dados nunca foram sincronizados. +- **Fix**: Criado script `migrate_aulas_contratos.cjs` que conecta diretamente ao PostgreSQL de produção e roda os INSERTs. +- **Resultado**: 109 aulas, 9 contratos, 1 modelo e 89 frequências migrados com sucesso. + +### 🐛 Bug 3: Aulas Órfãs (FK Constraint) +- **Causa**: 57 aulas no JSON pertenciam a uma turma deletada (`d48b268c-...`), causando violação de FK. +- **Fix**: O script filtra aulas cujo `classId` não existe na tabela `turmas`. + +### 🐛 Bug 4: Contratos Não Apareciam no Manager +- **Causa**: A função `getContratos()` retornava o campo como `date`, mas o frontend (`Contracts.tsx`) esperava `createdAt`. +- **Fix**: Corrigido o mapeamento em `database.js` para retornar `createdAt: r.created_at_fmt`. + +### 🐛 Bug 5: Classes.tsx Não Salvava Aulas no PostgreSQL +- **Causa**: Ao criar/editar turma com cronograma, as aulas geradas eram salvas **apenas no JSON** (`data.lessons`), nunca no banco. +- **Fix**: Adicionado `fetch('/api/aulas/lote')` no `Classes.tsx` para enviar as aulas geradas ao PostgreSQL. + +## Estado Final do Banco de Dados +| Tabela | Registros | +|---|---| +| alunos | 9 | +| aulas | 109 | +| contratos | 9 | +| frequencias | 89 | +| modelos_contrato | 1 | +| provas | 3 | +| turmas | 3 | + +## Padrões de Arquitetura +1. **Reverse Sync (Mão Dupla)**: Frontend salva no SQL via API e mantém backup no JSON. +2. **Fallback Gradual**: Portal prioriza SQL, invoca JSON apenas se banco retornar vazio. +3. **Migração de Dados**: Executada via script Node.js conectando diretamente ao PostgreSQL de produção (host: `150.230.87.131`, user: `edumanager`). diff --git a/manager/components/Classes.tsx b/manager/components/Classes.tsx index b964496..4518dec 100644 --- a/manager/components/Classes.tsx +++ b/manager/components/Classes.tsx @@ -204,7 +204,15 @@ const Classes: React.FC = ({ data, updateData, onNavigateToClass } if (!res.ok) throw new Error('Failed to create class'); } - // Save lessons in the json fallback for now since lessons are not fully migrated + // Save lessons in both PostgreSQL and JSON for compatibility + const classLessons = updatedLessons.filter(l => l.classId === newClass.id); + if (classLessons.length > 0) { + await fetch('/api/aulas/lote', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ aulas: classLessons }) + }).catch(e => console.warn('Erro ao salvar aulas no SQL:', e)); + } updateData({ lessons: updatedLessons }); dbService.saveData({ ...data, lessons: updatedLessons }); diff --git a/manager/components/Students.tsx b/manager/components/Students.tsx index 92e6235..51c5ad8 100644 --- a/manager/components/Students.tsx +++ b/manager/components/Students.tsx @@ -54,8 +54,8 @@ const Students: React.FC = ({ data, updateData, deepLinkStudentId useEffect(() => { loadStudents(); }, []); - const [dbClasses, setDbClasses] = useState(dbClasses || []); - const [dbCourses, setDbCourses] = useState(dbCourses || []); + const [dbClasses, setDbClasses] = useState(data?.classes || []); + const [dbCourses, setDbCourses] = useState(data?.courses || []); useEffect(() => { Promise.all([ diff --git a/manager/scratch/migrate_aulas_contratos.cjs b/manager/scratch/migrate_aulas_contratos.cjs new file mode 100644 index 0000000..25bd3f6 --- /dev/null +++ b/manager/scratch/migrate_aulas_contratos.cjs @@ -0,0 +1,130 @@ +const { Pool } = require('pg'); + +const pool = new Pool({ + host: '150.230.87.131', + port: 5432, + database: 'edumanager', + user: 'edumanager', + password: 'EduManager2026!Seguro', + ssl: false +}); + +async function migrate() { + const client = await pool.connect(); + try { + // 1. Ler school_data + const { rows } = await client.query('SELECT data FROM school_data LIMIT 1'); + if (!rows.length) { console.log('school_data vazio!'); return; } + const schoolData = rows[0].data; + + // Buscar turmas existentes para filtrar aulas órfãs + const { rows: turmasRows } = await client.query('SELECT id FROM turmas'); + const turmaIds = new Set(turmasRows.map(r => r.id)); + console.log(`Turmas no banco: ${turmaIds.size} → ${[...turmaIds].join(', ')}`); + + // 2. Migrar Aulas (filtrando só as que têm turma válida) + const lessons = schoolData.lessons || []; + const validLessons = lessons.filter(a => turmaIds.has(a.classId)); + const orphanLessons = lessons.filter(a => !turmaIds.has(a.classId)); + console.log(`\n📅 Aulas totais: ${lessons.length} | Válidas: ${validLessons.length} | Órfãs (turma deletada): ${orphanLessons.length}`); + + if (orphanLessons.length > 0) { + const orphanClasses = [...new Set(orphanLessons.map(l => l.classId))]; + console.log(` ⚠️ IDs de turmas deletadas: ${orphanClasses.join(', ')}`); + } + + let aulasOk = 0; + for (const a of validLessons) { + try { + await client.query( + `INSERT INTO aulas (id, turma_id, data, horario_inicio, horario_fim, status, tipo, motivo_cancelamento, aula_original_id) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT (id) DO NOTHING`, + [a.id, a.classId, a.date, a.startTime || null, a.endTime || null, a.status || 'scheduled', a.type || 'regular', a.cancelReason || null, a.originalLessonId || null] + ); + aulasOk++; + } catch (e) { + console.warn(` ⚠️ Aula ${a.id}: ${e.message}`); + } + } + console.log(` ✅ ${aulasOk}/${validLessons.length} aulas migradas!`); + + // 3. Migrar Contratos + const contracts = schoolData.contracts || []; + console.log(`\n📝 Migrando ${contracts.length} contratos...`); + let contratosOk = 0; + for (const c of contracts) { + try { + await client.query( + `INSERT INTO contratos (id, aluno_id, titulo, conteudo, created_at) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (id) DO NOTHING`, + [c.id, c.studentId, c.title, c.content, c.createdAt || new Date().toISOString()] + ); + contratosOk++; + } catch (e) { + console.warn(` ⚠️ Contrato ${c.id}: ${e.message}`); + } + } + console.log(` ✅ ${contratosOk}/${contracts.length} contratos migrados!`); + + // 4. Migrar Modelos de Contrato + const templates = schoolData.contractTemplates || []; + console.log(`\n📄 Migrando ${templates.length} modelos de contrato...`); + let templatesOk = 0; + for (const t of templates) { + try { + await client.query( + `INSERT INTO modelos_contrato (id, nome, conteudo) + VALUES ($1, $2, $3) + ON CONFLICT (id) DO UPDATE SET nome=EXCLUDED.nome, conteudo=EXCLUDED.conteudo`, + [t.id, t.name, t.content] + ); + templatesOk++; + } catch (e) { + console.warn(` ⚠️ Modelo ${t.id}: ${e.message}`); + } + } + console.log(` ✅ ${templatesOk}/${templates.length} modelos migrados!`); + + // 5. Migrar Frequências + const attendance = schoolData.attendance || []; + console.log(`\n📋 Migrando ${attendance.length} registros de frequência...`); + let freqOk = 0, freqSkip = 0; + for (const f of attendance) { + try { + await client.query( + `INSERT INTO frequencias (id, aula_id, turma_id, aluno_id, tipo, data_registro, url_anexo, justificado) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT (id) DO NOTHING`, + [f.id, f.lessonId || null, f.classId || null, f.studentId, f.type, f.date, f.attachment || null, f.justified || false] + ); + freqOk++; + } catch (e) { + freqSkip++; + } + } + console.log(` ✅ ${freqOk}/${attendance.length} frequências migradas! (${freqSkip} ignoradas)`); + + // Verificação Final + const checkAulas = await client.query('SELECT COUNT(*) as c FROM aulas'); + const checkContratos = await client.query('SELECT COUNT(*) as c FROM contratos'); + const checkModelos = await client.query('SELECT COUNT(*) as c FROM modelos_contrato'); + const checkFreq = await client.query('SELECT COUNT(*) as c FROM frequencias'); + + console.log('\n🎯 VERIFICAÇÃO FINAL:'); + console.log(` Aulas no banco: ${checkAulas.rows[0].c}`); + console.log(` Contratos no banco: ${checkContratos.rows[0].c}`); + console.log(` Modelos no banco: ${checkModelos.rows[0].c}`); + console.log(` Frequências no banco: ${checkFreq.rows[0].c}`); + console.log('\n✅ Migração completa!'); + + } catch (err) { + console.error('❌ ERRO FATAL:', err); + } finally { + client.release(); + await pool.end(); + } +} + +migrate(); diff --git a/manager/services/database.js b/manager/services/database.js index f8ef617..c795909 100644 --- a/manager/services/database.js +++ b/manager/services/database.js @@ -690,8 +690,8 @@ export async function deleteModeloContrato(id) { } export async function getContratos() { - const { rows } = await pool.query('SELECT *, TO_CHAR(created_at, \'YYYY-MM-DD"T"HH24:MI:SS"Z"\') as date FROM contratos ORDER BY created_at DESC'); - return rows.map(r => ({ id: r.id, studentId: r.aluno_id, title: r.titulo, content: r.conteudo, date: r.date })); + const { rows } = await pool.query('SELECT *, TO_CHAR(created_at, \'YYYY-MM-DD"T"HH24:MI:SS"Z"\') as created_at_fmt FROM contratos ORDER BY created_at DESC'); + return rows.map(r => ({ id: r.id, studentId: r.aluno_id, title: r.titulo, content: r.conteudo, createdAt: r.created_at_fmt })); } export async function insertContrato(c) {