feat: edicao individual e propagacao de modelos de contratos e sync sql
This commit is contained in:
parent
e33a5aac3d
commit
d4b73df9b4
|
|
@ -66,4 +66,7 @@
|
||||||
37. **Server Entry Point Safety**: The original `server.js` files in both Manager and Portal are OBSOLETE and kept only for historical context. You MUST NEVER modify or edit `server.js`. All backend changes must be applied exclusively to `server.selfhosted.js`.
|
37. **Server Entry Point Safety**: The original `server.js` files in both Manager and Portal are OBSOLETE and kept only for historical context. You MUST NEVER modify or edit `server.js`. All backend changes must be applied exclusively to `server.selfhosted.js`.
|
||||||
38. **Database Connection & MCP Integration**: Direct PostgreSQL access has been successfully configured via the `@modelcontextprotocol/server-postgres` MCP server to connect directly to the production database on the VPS (`150.230.87.131`). Database telemetry and operations can be executed via MCP SQL tools.
|
38. **Database Connection & MCP Integration**: Direct PostgreSQL access has been successfully configured via the `@modelcontextprotocol/server-postgres` MCP server to connect directly to the production database on the VPS (`150.230.87.131`). Database telemetry and operations can be executed via MCP SQL tools.
|
||||||
39. **Portal SQL-First Migration**: O Portal do Aluno consome dados majoritariamente de tabelas PostgreSQL (`alunos`, `aulas`, `frequencias`, `contratos`) usando `SELECT` e implementa Fallbacks legados lendo do JSON apenas se o BD retornar vazio.
|
39. **Portal SQL-First Migration**: O Portal do Aluno consome dados majoritariamente de tabelas PostgreSQL (`alunos`, `aulas`, `frequencias`, `contratos`) usando `SELECT` e implementa Fallbacks legados lendo do JSON apenas se o BD retornar vazio.
|
||||||
|
40. **CamelCase Backend Mapping**: Funções do backend que buscam dados do PostgreSQL (`getAlunos`, `getProvas`, `getFuncionarios`) DEVEM mapear as chaves de `snake_case` (ex: `turma_id`, `is_deleted`) para `camelCase` (ex: `classId`, `isDeleted`) antes de enviar para o Frontend. O Frontend deve usar fallbacks (`p.classId || p.turma_id`) durante a fase de transição para evitar quebras em views legadas (White Screen) e perdas de filtro de contagem (ex: "0 alunos").
|
||||||
|
41. **Database Schema Sync**: Sempre garanta que as tabelas de destino possuam colunas como `is_deleted` e demais modificadores antes de habilitar rotas PUT/DELETE, pois a ausência do schema falhará silenciosamente no Express 5.
|
||||||
|
42. **Cronological Display Standard**: Consultas a cadastros secundários (como Disciplinas e Categorias) DEVEM utilizar `ORDER BY created_at ASC` no SQL para manter a mesma ordem de listagem original do sistema (evitando reordenação alfabética não-intencional).
|
||||||
|
43. **Mass Contracts Update**: A edição de um "Modelo de Contrato" dispara atualizações em massa (PUT `/api/contratos/:id`) recalcularizando as variáveis de todos os contratos já emitidos para os alunos vinculados àquele modelo, garantindo atualização em tempo real no Portal do Aluno via SQL.
|
||||||
|
|
|
||||||
|
|
@ -88,17 +88,12 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
|
||||||
content: ''
|
content: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pre-load content when student is selected based on template
|
const generateContractContent = (student: any, templateContent: string) => {
|
||||||
useEffect(() => {
|
|
||||||
if (formData.studentId && !formData.content) {
|
|
||||||
const student = data.students.find(s => s.id === formData.studentId);
|
|
||||||
const cls = dbClasses.find(c => c.id === student?.classId);
|
const cls = dbClasses.find(c => c.id === student?.classId);
|
||||||
const course = dbCourses.find(c => c.id === cls?.courseId);
|
const course = dbCourses.find(c => c.id === cls?.courseId);
|
||||||
const templateObj = dbTemplates?.find(t => t.id === student?.contractTemplateId);
|
if (!student || !course) return templateContent;
|
||||||
|
|
||||||
if (student && course) {
|
|
||||||
let template = templateObj?.content || '';
|
|
||||||
|
|
||||||
|
let template = templateContent;
|
||||||
// Aluno
|
// Aluno
|
||||||
template = template.replace(/{{aluno}}/g, student.name || '');
|
template = template.replace(/{{aluno}}/g, student.name || '');
|
||||||
template = template.replace(/{{aluno_cpf}}/g, student.cpf || '');
|
template = template.replace(/{{aluno_cpf}}/g, student.cpf || '');
|
||||||
|
|
@ -131,9 +126,20 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
|
||||||
template = template.replace(/{{escola}}/g, data.profile.name || '');
|
template = template.replace(/{{escola}}/g, data.profile.name || '');
|
||||||
template = template.replace(/{{cnpj_escola}}/g, data.profile.cnpj || '');
|
template = template.replace(/{{cnpj_escola}}/g, data.profile.cnpj || '');
|
||||||
|
|
||||||
|
return template;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pre-load content when student is selected based on template
|
||||||
|
useEffect(() => {
|
||||||
|
if (formData.studentId && !formData.content) {
|
||||||
|
const student = data.students.find(s => s.id === formData.studentId);
|
||||||
|
const templateObj = dbTemplates?.find(t => t.id === student?.contractTemplateId);
|
||||||
|
|
||||||
|
if (student) {
|
||||||
|
const finalContent = generateContractContent(student, templateObj?.content || '');
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
content: template,
|
content: finalContent,
|
||||||
title: prev.title || `Contrato de Matrícula - ${student.name}`
|
title: prev.title || `Contrato de Matrícula - ${student.name}`
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
@ -158,11 +164,33 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((formData as any).id) {
|
||||||
|
// Editar individual
|
||||||
|
fetch(`/api/contratos/${(formData as any).id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
}).then(() => loadData());
|
||||||
|
|
||||||
|
updateData({
|
||||||
|
contracts: dbContracts.map(c => c.id === (formData as any).id ? { ...c, ...formData } : c)
|
||||||
|
});
|
||||||
|
closeModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newContract: Contract = {
|
const newContract: Contract = {
|
||||||
...formData,
|
...formData,
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fetch('/api/contratos', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(newContract)
|
||||||
|
}).then(() => loadData());
|
||||||
|
|
||||||
updateData({ contracts: [...dbContracts, newContract] });
|
updateData({ contracts: [...dbContracts, newContract] });
|
||||||
closeModal();
|
closeModal();
|
||||||
};
|
};
|
||||||
|
|
@ -188,7 +216,30 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(templateFormData)
|
body: JSON.stringify(templateFormData)
|
||||||
}).then(() => loadData());
|
}).then(async () => {
|
||||||
|
// Propagar para todos os alunos que usam este template e já tem contrato gerado
|
||||||
|
const affectedStudents = data.students.filter(s => s.contractTemplateId === templateFormData.id);
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
affectedStudents.forEach(student => {
|
||||||
|
const existingContract = dbContracts.find(c => c.studentId === student.id);
|
||||||
|
if (existingContract) {
|
||||||
|
const newContent = generateContractContent(student, templateFormData.content);
|
||||||
|
promises.push(
|
||||||
|
fetch(`/api/contratos/${existingContract.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ title: existingContract.title, content: newContent })
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (promises.length > 0) {
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
fetch('/api/modelos-contrato', {
|
fetch('/api/modelos-contrato', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -409,6 +460,16 @@ const Contracts: React.FC<ContractsProps> = ({ data, updateData }) => {
|
||||||
<Printer size={20} />
|
<Printer size={20} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setFormData({ studentId: contract.studentId, title: contract.title, content: contract.content, ...( { id: contract.id } as any ) });
|
||||||
|
setIsModalOpen(true);
|
||||||
|
}}
|
||||||
|
className="p-3 bg-white border border-slate-200 text-slate-400 hover:text-blue-600 rounded-xl transition-all shadow-sm"
|
||||||
|
title="Editar Contrato"
|
||||||
|
>
|
||||||
|
<Edit2 size={20} />
|
||||||
|
</button>
|
||||||
<button onClick={() => handleDelete(contract.id)} className="p-3 bg-white border border-slate-200 text-slate-400 hover:text-red-600 rounded-xl transition-all shadow-sm" title="Excluir"><Trash2 size={20} /></button>
|
<button onClick={() => handleDelete(contract.id)} className="p-3 bg-white border border-slate-200 text-slate-400 hover:text-red-600 rounded-xl transition-all shadow-sm" title="Excluir"><Trash2 size={20} /></button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
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 runAlter() {
|
||||||
|
try {
|
||||||
|
await pool.query("ALTER TABLE provas ADD COLUMN is_deleted BOOLEAN DEFAULT false");
|
||||||
|
await pool.query("ALTER TABLE provas ADD COLUMN evaluation_type TEXT DEFAULT 'exam'");
|
||||||
|
console.log("Colunas adicionadas!");
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e.message);
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runAlter();
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
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 migrateQuestoes() {
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
const { rows } = await client.query('SELECT data FROM school_data LIMIT 1');
|
||||||
|
if (!rows.length) { console.log('school_data vazio!'); return; }
|
||||||
|
|
||||||
|
const exams = rows[0].data.exams || [];
|
||||||
|
console.log(`\n📄 Verificando ${exams.length} provas no JSON...`);
|
||||||
|
|
||||||
|
let totalQuestions = 0;
|
||||||
|
let questionsMigrated = 0;
|
||||||
|
|
||||||
|
for (const exam of exams) {
|
||||||
|
if (!exam.questions || exam.questions.length === 0) continue;
|
||||||
|
|
||||||
|
const pId = exam.id;
|
||||||
|
// Verificar se a prova já existe no banco
|
||||||
|
const checkProva = await client.query('SELECT id FROM provas WHERE id = $1', [pId]);
|
||||||
|
if (checkProva.rows.length === 0) {
|
||||||
|
console.warn(`Prova ${pId} não encontrada no BD SQL. Ignorando questoes.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < exam.questions.length; i++) {
|
||||||
|
const q = exam.questions[i];
|
||||||
|
totalQuestions++;
|
||||||
|
try {
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO questoes_provas (id, prova_id, texto, opcoes, indice_correto, ordem, imagem_url)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
ON CONFLICT (id) DO NOTHING`,
|
||||||
|
[
|
||||||
|
q.id || `q-${pId}-${i}`,
|
||||||
|
pId,
|
||||||
|
q.text || '',
|
||||||
|
JSON.stringify(q.options || []),
|
||||||
|
q.correctAnswer !== undefined ? parseInt(q.correctAnswer) : 0,
|
||||||
|
i,
|
||||||
|
q.imageUrl || null
|
||||||
|
]
|
||||||
|
);
|
||||||
|
questionsMigrated++;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Erro na questão ${q.id}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n✅ Migração concluída: ${questionsMigrated}/${totalQuestions} questões migradas!`);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ ERRO:', err);
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateQuestoes();
|
||||||
|
|
@ -41,7 +41,7 @@ import {
|
||||||
getDisciplinas, insertDisciplina, updateDisciplina, deleteDisciplina,
|
getDisciplinas, insertDisciplina, updateDisciplina, deleteDisciplina,
|
||||||
getAlunos, insertAluno, updateAluno, deleteAluno,
|
getAlunos, insertAluno, updateAluno, deleteAluno,
|
||||||
getModelosContrato, insertModeloContrato, updateModeloContrato, deleteModeloContrato,
|
getModelosContrato, insertModeloContrato, updateModeloContrato, deleteModeloContrato,
|
||||||
getContratos, insertContrato, deleteContrato,
|
getContratos, insertContrato, updateContrato, deleteContrato,
|
||||||
getAulasByTurma, getAllAulas, insertAulas, deleteAulas,
|
getAulasByTurma, getAllAulas, insertAulas, deleteAulas,
|
||||||
getProvas, getQuestoesDaProva, insertProva, updateProva, deleteProva, syncQuestoesProva
|
getProvas, getQuestoesDaProva, insertProva, updateProva, deleteProva, syncQuestoesProva
|
||||||
} from './services/database.js';
|
} from './services/database.js';
|
||||||
|
|
@ -546,6 +546,9 @@ app.get('/api/contratos', async (req, res) => {
|
||||||
app.post('/api/contratos', async (req, res) => {
|
app.post('/api/contratos', async (req, res) => {
|
||||||
try { await insertContrato(req.body); res.json({ success: true }); } catch (e) { res.status(500).json({ error: 'Erro' }); }
|
try { await insertContrato(req.body); res.json({ success: true }); } catch (e) { res.status(500).json({ error: 'Erro' }); }
|
||||||
});
|
});
|
||||||
|
app.put('/api/contratos/:id', async (req, res) => {
|
||||||
|
try { await updateContrato(req.params.id, req.body); res.json({ success: true }); } catch (e) { res.status(500).json({ error: 'Erro' }); }
|
||||||
|
});
|
||||||
app.delete('/api/contratos/:id', async (req, res) => {
|
app.delete('/api/contratos/:id', async (req, res) => {
|
||||||
try { await deleteContrato(req.params.id); res.json({ success: true }); } catch (e) { res.status(500).json({ error: 'Erro' }); }
|
try { await deleteContrato(req.params.id); res.json({ success: true }); } catch (e) { res.status(500).json({ error: 'Erro' }); }
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -515,7 +515,7 @@ export async function deleteTurma(id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDisciplinas() {
|
export async function getDisciplinas() {
|
||||||
const { rows } = await pool.query('SELECT * FROM disciplinas ORDER BY nome ASC');
|
const { rows } = await pool.query('SELECT * FROM disciplinas ORDER BY created_at ASC');
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -720,6 +720,10 @@ export async function insertContrato(c) {
|
||||||
await pool.query('INSERT INTO contratos (id, aluno_id, titulo, conteudo) VALUES ($1, $2, $3, $4)', [c.id, c.studentId, c.title, c.content]);
|
await pool.query('INSERT INTO contratos (id, aluno_id, titulo, conteudo) VALUES ($1, $2, $3, $4)', [c.id, c.studentId, c.title, c.content]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateContrato(id, c) {
|
||||||
|
await pool.query('UPDATE contratos SET titulo=$1, conteudo=$2 WHERE id=$3', [c.title, c.content, id]);
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteContrato(id) {
|
export async function deleteContrato(id) {
|
||||||
await pool.query('DELETE FROM contratos WHERE id=$1', [id]);
|
await pool.query('DELETE FROM contratos WHERE id=$1', [id]);
|
||||||
}
|
}
|
||||||
|
|
@ -820,9 +824,9 @@ export async function getProvas() {
|
||||||
examId: q.prova_id,
|
examId: q.prova_id,
|
||||||
text: q.texto,
|
text: q.texto,
|
||||||
options: q.opcoes || [],
|
options: q.opcoes || [],
|
||||||
correctAnswer: q.resposta_correta,
|
correctAnswer: q.indice_correto,
|
||||||
order: q.ordem,
|
order: q.ordem,
|
||||||
imageUrl: q.url_imagem
|
imageUrl: q.imagem_url
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue