fix: resolve bugs da tela de alunos, contratos vazios e salva aulas geradas no bd
This commit is contained in:
parent
65119df2f2
commit
bc440d7dbe
77
MEMORY.md
77
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
|
## Visão Geral
|
||||||
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).
|
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)
|
## Módulos Migrados
|
||||||
- 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.
|
|
||||||
|
|
||||||
### 2. Frontend (Manager)
|
### Fase 4: Gestão de Alunos e Autenticação
|
||||||
- `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`.
|
- **Backend Manager**: CRUD no `database.js` para tabela `alunos`, rotas `/api/alunos` no `server.selfhosted.js`.
|
||||||
- `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.
|
- **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
|
### Fase 5: Avaliações e Provas
|
||||||
- **`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).
|
- Backend e Frontend conectados às tabelas `provas` e `questoes_provas`.
|
||||||
- **`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.
|
|
||||||
|
|
||||||
## Impacto
|
### Fase 6: Cronograma e Aulas
|
||||||
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.
|
- **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<any[]>(dbClasses || [])` referenciava a si mesma.
|
||||||
|
- **Fix**: Alterado para `useState<any[]>(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`).
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,15 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
|
||||||
if (!res.ok) throw new Error('Failed to create class');
|
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 });
|
updateData({ lessons: updatedLessons });
|
||||||
dbService.saveData({ ...data, lessons: updatedLessons });
|
dbService.saveData({ ...data, lessons: updatedLessons });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,8 @@ const Students: React.FC<StudentsProps> = ({ data, updateData, deepLinkStudentId
|
||||||
|
|
||||||
useEffect(() => { loadStudents(); }, []);
|
useEffect(() => { loadStudents(); }, []);
|
||||||
|
|
||||||
const [dbClasses, setDbClasses] = useState<any[]>(dbClasses || []);
|
const [dbClasses, setDbClasses] = useState<any[]>(data?.classes || []);
|
||||||
const [dbCourses, setDbCourses] = useState<any[]>(dbCourses || []);
|
const [dbCourses, setDbCourses] = useState<any[]>(data?.courses || []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -690,8 +690,8 @@ export async function deleteModeloContrato(id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getContratos() {
|
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');
|
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, date: r.date }));
|
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) {
|
export async function insertContrato(c) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue