1334 lines
52 KiB
JavaScript
1334 lines
52 KiB
JavaScript
/**
|
||
* ============================================================
|
||
* SERVIÇO DE BANCO DE DADOS — PostgreSQL (Self-Hosted)
|
||
* Substitui todas as chamadas supabase.from(...) do sistema
|
||
* ============================================================
|
||
*/
|
||
import pg from 'pg';
|
||
|
||
// Registrar parser global para tipo NUMERIC (OID 1700) para retornar como Number
|
||
pg.types.setTypeParser(1700, (val) => val === null ? null : parseFloat(val));
|
||
|
||
const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://edumanager:EduManager2026!Seguro@postgres:5432/edumanager';
|
||
|
||
const pool = new pg.Pool({
|
||
connectionString: DATABASE_URL,
|
||
max: 20,
|
||
idleTimeoutMillis: 30000,
|
||
connectionTimeoutMillis: 5000,
|
||
});
|
||
|
||
pool.on('error', (err) => {
|
||
console.error('[PostgreSQL] Erro inesperado no pool:', err);
|
||
});
|
||
|
||
// ============================================================
|
||
// HELPER: Buscar school_data JSON blob (compatibilidade legada)
|
||
// ============================================================
|
||
export async function getSchoolData() {
|
||
const { rows } = await pool.query(
|
||
'SELECT data FROM school_data WHERE id = 1'
|
||
);
|
||
return rows[0]?.data || {};
|
||
}
|
||
|
||
/**
|
||
* Percorre as aulas concluídas e gera registros de falta para alunos que não compareceram.
|
||
* Isso transforma faltas "virtuais" em registros físicos no banco de dados.
|
||
*/
|
||
export async function processAutoAbsences(data) {
|
||
if (!data.lessons || !data.students || !data.attendance) return data;
|
||
|
||
const now = new Date();
|
||
let updated = false;
|
||
|
||
// 1. Auto-correção/Autolimpeza: remove faltas auto-geradas duplicadas se o aluno tiver uma presença registrada no mesmo dia/aula
|
||
const initialLength = data.attendance.length;
|
||
data.attendance = data.attendance.filter(a => {
|
||
if (a.type === 'absence' && (a.autoGenerated || (a.id && a.id.startsWith('auto-abs-')))) {
|
||
const hasPresence = data.attendance.some(p =>
|
||
p.studentId === a.studentId &&
|
||
(p.type === 'presence' || !p.type) &&
|
||
(p.lessonId === a.lessonId || (p.date && a.date && p.date.substring(0, 10) === a.date.substring(0, 10)))
|
||
);
|
||
return !hasPresence;
|
||
}
|
||
return true;
|
||
});
|
||
if (data.attendance.length !== initialLength) {
|
||
updated = true;
|
||
}
|
||
|
||
// Cleanup SQL orphaned absences as well, so SQL and JSON are consistently cleaned
|
||
try {
|
||
const client = await pool.connect();
|
||
try {
|
||
await client.query(`
|
||
DELETE FROM frequencias a
|
||
WHERE a.tipo = 'absence' AND a.id LIKE 'auto-abs-%'
|
||
AND EXISTS (
|
||
SELECT 1 FROM frequencias p
|
||
WHERE p.tipo = 'presence'
|
||
AND p.aluno_id = a.aluno_id
|
||
AND (p.aula_id = a.aula_id OR DATE(p.data AT TIME ZONE 'UTC') = DATE(a.data AT TIME ZONE 'UTC'))
|
||
)
|
||
`);
|
||
} finally {
|
||
client.release();
|
||
}
|
||
} catch (err) {
|
||
console.warn('[Database:AutoAbsences] Erro ao limpar faltas órfãs no SQL:', err.message);
|
||
}
|
||
|
||
// Cache de alunos por turma para performance
|
||
const studentsByClass = {};
|
||
|
||
data.lessons.forEach(lesson => {
|
||
// RESOLUÇÃO DE FUSO HORÁRIO: Força fuso horário América/São_Paulo (-03:00) ao analisar a hora de término da aula no servidor (geralmente UTC)
|
||
const lessonEndStr = `${lesson.date}T${lesson.endTime || '23:59'}:00-03:00`;
|
||
const lessonEnd = new Date(lessonEndStr);
|
||
|
||
if (now > lessonEnd && lesson.status !== 'cancelled') {
|
||
if (!studentsByClass[lesson.classId]) {
|
||
studentsByClass[lesson.classId] = data.students.filter(s => s.classId === lesson.classId && s.status === 'active');
|
||
}
|
||
|
||
studentsByClass[lesson.classId].forEach(student => {
|
||
const hasRecord = data.attendance.some(a =>
|
||
a.studentId === student.id &&
|
||
(a.lessonId === lesson.id || (a.date && a.date.startsWith(lesson.date)))
|
||
);
|
||
|
||
if (!hasRecord) {
|
||
data.attendance.push({
|
||
id: `auto-abs-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
|
||
studentId: student.id,
|
||
classId: lesson.classId,
|
||
lessonId: lesson.id,
|
||
date: `${lesson.date}T${lesson.startTime || '00:00'}:00`,
|
||
type: 'absence',
|
||
verified: true,
|
||
autoGenerated: true
|
||
});
|
||
updated = true;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
if (updated) {
|
||
data.lastUpdated = new Date().toISOString();
|
||
}
|
||
return data;
|
||
}
|
||
|
||
export async function saveSchoolData(data) {
|
||
// Aplicar fechamento de pauta automático antes de salvar
|
||
const dataWithAbsences = await processAutoAbsences(data);
|
||
|
||
await pool.query(
|
||
`INSERT INTO school_data (id, data, updated_at)
|
||
VALUES (1, $1, NOW())
|
||
ON CONFLICT (id) DO UPDATE SET data = $1, updated_at = NOW()`,
|
||
[JSON.stringify(dataWithAbsences)]
|
||
);
|
||
|
||
// Sincronizar tabelas relacionais em background para não travar o salvamento
|
||
syncJsonToRelationalTables(dataWithAbsences).catch(err =>
|
||
console.error('[Database:Sync] Erro na sincronização automática:', err)
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// HELPERS: alunos_cobrancas
|
||
// ============================================================
|
||
export async function insertCobrancas(cobrancas) {
|
||
const client = await pool.connect();
|
||
try {
|
||
await client.query('BEGIN');
|
||
for (const c of cobrancas) {
|
||
await client.query(
|
||
`INSERT INTO alunos_cobrancas
|
||
(aluno_id, asaas_customer_id, asaas_payment_id, asaas_installment_id, installment, valor, vencimento, link_boleto, amount_original)
|
||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||
[c.aluno_id, c.asaas_customer_id, c.asaas_payment_id, c.asaas_installment_id || c.installment, c.installment, c.valor, c.vencimento, c.link_boleto, c.valor]
|
||
);
|
||
}
|
||
|
||
|
||
// --- SYNC MODELOS CONTRATO ---
|
||
if (schoolData.contractTemplates && schoolData.contractTemplates.length > 0) {
|
||
for (const t of schoolData.contractTemplates) {
|
||
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]
|
||
).catch(err => console.warn(`[Sync:Modelos] Erro ${t.id}:`, err.message));
|
||
}
|
||
}
|
||
|
||
// --- SYNC CONTRATOS ---
|
||
if (schoolData.contracts && schoolData.contracts.length > 0) {
|
||
for (const c of schoolData.contracts) {
|
||
await client.query(
|
||
`INSERT INTO contratos (id, aluno_id, titulo, conteudo, created_at) VALUES ($1, $2, $3, $4, $5)
|
||
ON CONFLICT (id) DO UPDATE SET titulo=EXCLUDED.titulo, conteudo=EXCLUDED.conteudo`,
|
||
[c.id, c.studentId, c.title, c.content, c.createdAt]
|
||
).catch(err => console.warn(`[Sync:Contratos] Erro ${c.id}:`, err.message));
|
||
}
|
||
}
|
||
|
||
// --- SYNC AULAS ---
|
||
if (schoolData.lessons && schoolData.lessons.length > 0) {
|
||
for (const a of schoolData.lessons) {
|
||
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 UPDATE SET
|
||
turma_id = EXCLUDED.turma_id,
|
||
data = EXCLUDED.data,
|
||
horario_inicio = COALESCE(EXCLUDED.horario_inicio, aulas.horario_inicio),
|
||
horario_fim = COALESCE(EXCLUDED.horario_fim, aulas.horario_fim),
|
||
status = EXCLUDED.status,
|
||
tipo = EXCLUDED.tipo,
|
||
motivo_cancelamento = EXCLUDED.motivo_cancelamento,
|
||
aula_original_id = COALESCE(EXCLUDED.aula_original_id, aulas.aula_original_id)`,
|
||
[
|
||
a.id, a.classId, a.date, a.startTime || null, a.endTime || null, a.status || 'scheduled', a.type || 'regular',
|
||
a.cancelReason || null, a.originalLessonId || null
|
||
]
|
||
).catch(err => console.warn(`[Sync:Aulas] Erro na aula ${a.id}:`, err.message));
|
||
}
|
||
}
|
||
|
||
// --- SYNC FREQUENCIAS ---
|
||
if (schoolData.attendance && schoolData.attendance.length > 0) {
|
||
for (const f of schoolData.attendance) {
|
||
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 UPDATE SET
|
||
tipo = EXCLUDED.tipo,
|
||
url_anexo = COALESCE(EXCLUDED.url_anexo, frequencias.url_anexo),
|
||
justificado = EXCLUDED.justificado`,
|
||
[
|
||
f.id, f.lessonId, f.classId, f.studentId, f.type, f.date, f.attachment || null, f.justified || false
|
||
]
|
||
).catch(err => console.warn(`[Sync:Freq] Erro na freq ${f.id}:`, err.message));
|
||
}
|
||
}
|
||
|
||
await client.query('COMMIT');
|
||
} catch (e) {
|
||
await client.query('ROLLBACK');
|
||
throw e;
|
||
} finally {
|
||
client.release();
|
||
}
|
||
}
|
||
|
||
export async function updateCobranca(asaasPaymentId, updateData) {
|
||
const setClauses = [];
|
||
const values = [];
|
||
let i = 1;
|
||
|
||
for (const [key, value] of Object.entries(updateData)) {
|
||
if (value !== undefined) {
|
||
setClauses.push(`${key} = $${i}`);
|
||
values.push(value);
|
||
i++;
|
||
}
|
||
}
|
||
|
||
if (setClauses.length === 0) return;
|
||
|
||
values.push(asaasPaymentId);
|
||
await pool.query(
|
||
`UPDATE alunos_cobrancas SET ${setClauses.join(', ')} WHERE asaas_payment_id = $${i}`,
|
||
values
|
||
);
|
||
}
|
||
|
||
export async function deleteCobranca(asaasPaymentId) {
|
||
await pool.query(
|
||
'DELETE FROM alunos_cobrancas WHERE asaas_payment_id = $1',
|
||
[asaasPaymentId]
|
||
);
|
||
}
|
||
|
||
export async function getCobrancaByPaymentId(asaasPaymentId) {
|
||
const { rows } = await pool.query(
|
||
'SELECT * FROM alunos_cobrancas WHERE asaas_payment_id = $1',
|
||
[asaasPaymentId]
|
||
);
|
||
return rows[0] || null;
|
||
}
|
||
|
||
export async function getCobrancasByOrQuery(id) {
|
||
// Replicates: supabase.from('alunos_cobrancas').select('*').or(...)
|
||
const { rows } = await pool.query(
|
||
`SELECT * FROM alunos_cobrancas
|
||
WHERE installment = $1
|
||
OR asaas_installment_id = $1
|
||
OR asaas_payment_id = $1
|
||
OR id::text = $1
|
||
ORDER BY vencimento ASC`,
|
||
[id]
|
||
);
|
||
return rows;
|
||
}
|
||
|
||
export async function getCobrancasByAlunoId(alunoId) {
|
||
const { rows } = await pool.query(
|
||
'SELECT * FROM alunos_cobrancas WHERE aluno_id = $1 ORDER BY vencimento ASC',
|
||
[alunoId]
|
||
);
|
||
return rows;
|
||
}
|
||
|
||
export async function getCobrancasPendentes() {
|
||
const { rows } = await pool.query(
|
||
"SELECT * FROM alunos_cobrancas WHERE status = 'PENDENTE'"
|
||
);
|
||
return rows;
|
||
}
|
||
|
||
export async function getCobrancasAtrasadas() {
|
||
const { rows } = await pool.query(
|
||
"SELECT * FROM alunos_cobrancas WHERE status = 'ATRASADO'"
|
||
);
|
||
return rows;
|
||
}
|
||
|
||
export async function getCobrancasByInstallmentId(installmentId) {
|
||
const { rows } = await pool.query(
|
||
'SELECT * FROM alunos_cobrancas WHERE asaas_installment_id = $1 ORDER BY vencimento ASC',
|
||
[installmentId]
|
||
);
|
||
return rows;
|
||
}
|
||
|
||
export async function updateCobrancaLinkCarne(installmentId, linkCarne) {
|
||
await pool.query(
|
||
'UPDATE alunos_cobrancas SET link_carne = $1 WHERE asaas_installment_id = $2',
|
||
[linkCarne, installmentId]
|
||
);
|
||
}
|
||
|
||
export async function updateCobrancaByField(field, id, updateData) {
|
||
const setClauses = [];
|
||
const values = [];
|
||
let i = 1;
|
||
|
||
for (const [key, value] of Object.entries(updateData)) {
|
||
if (value !== undefined) {
|
||
setClauses.push(`${key} = $${i}`);
|
||
values.push(value);
|
||
i++;
|
||
}
|
||
}
|
||
|
||
if (setClauses.length === 0) return;
|
||
|
||
values.push(id);
|
||
await pool.query(
|
||
`UPDATE alunos_cobrancas SET ${setClauses.join(', ')} WHERE ${field} = $${i}`,
|
||
values
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// HELPERS: provas_submissoes
|
||
// ============================================================
|
||
export async function getSubmissoesByAluno(alunoId) {
|
||
const { rows } = await pool.query(
|
||
'SELECT * FROM provas_submissoes WHERE aluno_id = $1',
|
||
[alunoId]
|
||
);
|
||
return rows;
|
||
}
|
||
|
||
export async function getSubmissaoByAlunoAndExam(alunoId, examId) {
|
||
const { rows } = await pool.query(
|
||
'SELECT id FROM provas_submissoes WHERE aluno_id = $1 AND prova_id = $2 LIMIT 1',
|
||
[alunoId, examId]
|
||
);
|
||
return rows;
|
||
}
|
||
|
||
export async function insertSubmissao(submission) {
|
||
await pool.query(
|
||
`INSERT INTO provas_submissoes (aluno_id, prova_id, total_questoes, acertos, erros, percentual, nota_final, respostas, created_at)
|
||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||
[
|
||
submission.aluno_id, submission.exam_id,
|
||
submission.total_questions, submission.correct_count, submission.wrong_count,
|
||
submission.percentage, submission.final_score,
|
||
JSON.stringify(submission.answers_json), submission.created_at
|
||
]
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// HELPERS: notas_boletim
|
||
// ============================================================
|
||
export async function initNotasTable() {
|
||
// Remover constraints restritivas da tabela de submissões se existirem (transição JSON -> Postgres)
|
||
try {
|
||
await pool.query(`
|
||
ALTER TABLE provas_submissoes DROP CONSTRAINT IF EXISTS provas_submissoes_aluno_id_fkey;
|
||
ALTER TABLE provas_submissoes DROP CONSTRAINT IF EXISTS provas_submissoes_prova_id_fkey;
|
||
`);
|
||
} catch (err) {
|
||
console.log('[PostgreSQL] ℹ️ Submissoes fkey já removidas ou tabela não existe.');
|
||
}
|
||
|
||
// Garantir unicidade do asaas_payment_id para permitir ON CONFLICT
|
||
try {
|
||
await pool.query(`
|
||
DO $$
|
||
BEGIN
|
||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'unique_asaas_payment_id') THEN
|
||
ALTER TABLE alunos_cobrancas ADD CONSTRAINT unique_asaas_payment_id UNIQUE (asaas_payment_id);
|
||
END IF;
|
||
END $$;
|
||
`);
|
||
} catch (err) {
|
||
console.warn('[PostgreSQL] Erro ao garantir UNIQUE em alunos_cobrancas:', err.message);
|
||
}
|
||
|
||
await pool.query(`
|
||
CREATE TABLE IF NOT EXISTS notas_boletim (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
aluno_id VARCHAR(255) NOT NULL,
|
||
disciplina_id VARCHAR(255) NOT NULL,
|
||
periodo_id VARCHAR(255) NOT NULL,
|
||
prova_id VARCHAR(255),
|
||
valor NUMERIC(5, 2) NOT NULL,
|
||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||
UNIQUE(aluno_id, disciplina_id, periodo_id, prova_id)
|
||
);
|
||
`);
|
||
}
|
||
|
||
export async function getNotasByAluno(alunoId) {
|
||
const { rows } = await pool.query(
|
||
'SELECT * FROM notas_boletim WHERE aluno_id = $1',
|
||
[alunoId]
|
||
);
|
||
return rows;
|
||
}
|
||
|
||
export async function upsertNota(nota) {
|
||
// Trata prova_id null se for direta para o unique index funcionar de forma previsível (PostgreSQL 15+ tem NULLS NOT DISTINCT, mas para garantir via app logic vamos usar uma abordagem de ON CONFLICT)
|
||
// No caso do PostgreSQL padrão, múltiplos NULLs não dão conflito no UNIQUE.
|
||
// Para contornar e permitir upsert real, faremos DELETE e INSERT ou garantiremos que o código gerencie o NULL logicamente.
|
||
|
||
if (nota.prova_id) {
|
||
await pool.query(
|
||
`INSERT INTO notas_boletim (aluno_id, disciplina_id, periodo_id, prova_id, valor, updated_at)
|
||
VALUES ($1, $2, $3, $4, $5, NOW())
|
||
ON CONFLICT (aluno_id, disciplina_id, periodo_id, prova_id)
|
||
DO UPDATE SET valor = EXCLUDED.valor, updated_at = NOW()`,
|
||
[nota.aluno_id, nota.disciplina_id, nota.periodo_id, nota.prova_id, nota.valor]
|
||
);
|
||
} else {
|
||
// Para notas diretas, se existir apagamos e inserimos (pois o unique index normal não restringe múltiplos nulls)
|
||
await pool.query(
|
||
`DELETE FROM notas_boletim WHERE aluno_id = $1 AND disciplina_id = $2 AND periodo_id = $3 AND prova_id IS NULL`,
|
||
[nota.aluno_id, nota.disciplina_id, nota.periodo_id]
|
||
);
|
||
await pool.query(
|
||
`INSERT INTO notas_boletim (aluno_id, disciplina_id, periodo_id, prova_id, valor, updated_at)
|
||
VALUES ($1, $2, $3, NULL, $4, NOW())`,
|
||
[nota.aluno_id, nota.disciplina_id, nota.periodo_id, nota.valor]
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function deleteNotasManuaisAusentes(alunoId, notasManuaisRetidas) {
|
||
// Para limpar notas que o professor apagou (vazio) no manager
|
||
// notasManuaisRetidas é um array de objetos { disciplina_id, periodo_id, prova_id }
|
||
// Implementaremos a limpeza iterativamente na rota
|
||
}
|
||
|
||
// ============================================================
|
||
// HELPERS: cursos e turmas
|
||
// ============================================================
|
||
export async function getCursos() {
|
||
const { rows } = await pool.query('SELECT * FROM cursos ORDER BY nome ASC');
|
||
return rows;
|
||
}
|
||
|
||
export async function insertCurso(c) {
|
||
await pool.query(
|
||
`INSERT INTO cursos (id, nome, duracao, duracao_meses, taxa_matricula, mensalidade, descricao, multa_percentual, juros_percentual)
|
||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||
[c.id, c.nome, c.duracao || '', c.duracao_meses || 12, c.taxa_matricula || 0, c.mensalidade || 0, c.descricao || '', c.multa_percentual || 0, c.juros_percentual || 0]
|
||
);
|
||
}
|
||
|
||
export async function updateCurso(id, updateData) {
|
||
const setClauses = [];
|
||
const values = [];
|
||
let i = 1;
|
||
|
||
for (const [key, value] of Object.entries(updateData)) {
|
||
if (value !== undefined) {
|
||
setClauses.push(`${key} = $${i}`);
|
||
values.push(value);
|
||
i++;
|
||
}
|
||
}
|
||
|
||
if (setClauses.length === 0) return;
|
||
|
||
values.push(id);
|
||
await pool.query(
|
||
`UPDATE cursos SET ${setClauses.join(', ')} WHERE id = $${i}`,
|
||
values
|
||
);
|
||
}
|
||
|
||
export async function deleteCurso(id) {
|
||
await pool.query('DELETE FROM cursos WHERE id = $1', [id]);
|
||
}
|
||
|
||
export async function getTurmas() {
|
||
const { rows } = await pool.query('SELECT * FROM turmas ORDER BY nome ASC');
|
||
return rows;
|
||
}
|
||
|
||
export async function insertTurma(t) {
|
||
await pool.query(
|
||
`INSERT INTO turmas (id, nome, curso_id, professor, horario, dia_semana, max_alunos, data_inicio, data_fim, horario_inicio_padrao, horario_fim_padrao)
|
||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
|
||
[t.id, t.nome, t.curso_id || null, t.professor || '', t.horario || '', t.dia_semana || null, t.max_alunos || 30, t.data_inicio || null, t.data_fim || null, t.horario_inicio_padrao || null, t.horario_fim_padrao || null]
|
||
);
|
||
}
|
||
|
||
export async function updateTurma(id, updateData) {
|
||
const setClauses = [];
|
||
const values = [];
|
||
let i = 1;
|
||
|
||
for (const [key, value] of Object.entries(updateData)) {
|
||
if (value !== undefined) {
|
||
setClauses.push(`${key} = $${i}`);
|
||
values.push(value);
|
||
i++;
|
||
}
|
||
}
|
||
|
||
if (setClauses.length === 0) return;
|
||
|
||
values.push(id);
|
||
await pool.query(
|
||
`UPDATE turmas SET ${setClauses.join(', ')} WHERE id = $${i}`,
|
||
values
|
||
);
|
||
}
|
||
|
||
export async function deleteTurma(id) {
|
||
await pool.query('DELETE FROM turmas WHERE id = $1', [id]);
|
||
}
|
||
|
||
export async function getDisciplinas() {
|
||
const { rows } = await pool.query('SELECT * FROM disciplinas ORDER BY created_at ASC');
|
||
return rows;
|
||
}
|
||
|
||
export async function insertDisciplina(d) {
|
||
await pool.query(
|
||
`INSERT INTO disciplinas (id, nome) VALUES ($1, $2)`,
|
||
[d.id, d.nome]
|
||
);
|
||
}
|
||
|
||
export async function updateDisciplina(id, updateData) {
|
||
if (updateData.nome !== undefined) {
|
||
await pool.query(
|
||
`UPDATE disciplinas SET nome = $1 WHERE id = $2`,
|
||
[updateData.nome, id]
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function deleteDisciplina(id) {
|
||
await pool.query('DELETE FROM disciplinas WHERE id = $1', [id]);
|
||
}
|
||
|
||
// ============================================================
|
||
// HELPERS: funcionarios e categorias_funcionarios
|
||
// ============================================================
|
||
export async function getFuncionarios() {
|
||
const { rows } = await pool.query('SELECT * FROM funcionarios ORDER BY nome ASC');
|
||
return rows.map(r => ({
|
||
id: r.id,
|
||
name: r.nome,
|
||
cpf: r.cpf,
|
||
email: r.email,
|
||
phone: r.telefone,
|
||
categoryId: r.categoria_id,
|
||
hireDate: r.data_admissao,
|
||
createdAt: r.created_at
|
||
}));
|
||
}
|
||
|
||
export async function getCategoriasFuncionarios() {
|
||
const { rows } = await pool.query('SELECT * FROM categorias_funcionarios ORDER BY nome ASC');
|
||
return rows.map(r => ({
|
||
id: r.id,
|
||
name: r.nome,
|
||
createdAt: r.created_at
|
||
}));
|
||
}
|
||
|
||
export async function insertFuncionario(f) {
|
||
await pool.query(
|
||
`INSERT INTO funcionarios (id, nome, cpf, email, telefone, categoria_id, data_admissao)
|
||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||
[f.id, f.nome, f.cpf, f.email, f.telefone, f.categoria_id, f.data_admissao || null]
|
||
);
|
||
}
|
||
|
||
export async function updateFuncionario(id, updateData) {
|
||
const setClauses = [];
|
||
const values = [];
|
||
let i = 1;
|
||
|
||
for (const [key, value] of Object.entries(updateData)) {
|
||
if (value !== undefined) {
|
||
setClauses.push(`${key} = $${i}`);
|
||
values.push(value);
|
||
i++;
|
||
}
|
||
}
|
||
|
||
if (setClauses.length === 0) return;
|
||
|
||
values.push(id);
|
||
await pool.query(
|
||
`UPDATE funcionarios SET ${setClauses.join(', ')} WHERE id = $${i}`,
|
||
values
|
||
);
|
||
}
|
||
|
||
export async function deleteFuncionario(id) {
|
||
await pool.query('DELETE FROM funcionarios WHERE id = $1', [id]);
|
||
}
|
||
|
||
export async function insertCategoriaFuncionario(c) {
|
||
await pool.query(
|
||
`INSERT INTO categorias_funcionarios (id, nome) VALUES ($1, $2)`,
|
||
[c.id, c.nome]
|
||
);
|
||
}
|
||
|
||
export async function updateCategoriaFuncionario(id, nome) {
|
||
await pool.query(
|
||
`UPDATE categorias_funcionarios SET nome = $1 WHERE id = $2`,
|
||
[nome, id]
|
||
);
|
||
}
|
||
|
||
export async function deleteCategoriaFuncionario(id) {
|
||
await pool.query('DELETE FROM categorias_funcionarios WHERE id = $1', [id]);
|
||
}
|
||
|
||
// ============================================================
|
||
// ALUNOS (FASE 4)
|
||
// ============================================================
|
||
export async function getAlunos() {
|
||
const result = await pool.query("SELECT * FROM alunos ORDER BY nome ASC");
|
||
return result.rows.map(r => ({
|
||
...r,
|
||
classId: r.turma_id,
|
||
name: r.nome,
|
||
status: r.status,
|
||
cpf: r.cpf,
|
||
phone: r.telefone,
|
||
registrationDate: r.data_matricula,
|
||
contractTemplateId: r.modelo_contrato_id,
|
||
faceDescriptor: r.face_descriptor
|
||
}));
|
||
}
|
||
|
||
export async function insertAluno(a) {
|
||
const result = await pool.query(
|
||
`INSERT INTO alunos (
|
||
id, nome, email, telefone, data_nascimento, cpf, rg, rg_data_emissao,
|
||
nome_responsavel, telefone_responsavel, cpf_responsavel, data_nascimento_responsavel,
|
||
turma_id, status, data_matricula, foto_url, cep, rua, numero, bairro, cidade, estado,
|
||
desconto, tem_responsavel, modelo_contrato_id, numero_matricula, senha_portal,
|
||
motivo_cancelamento, face_descriptor
|
||
) VALUES (
|
||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29
|
||
) RETURNING *`,
|
||
[
|
||
a.id, a.nome || a.name, a.email || '', a.telefone || a.phone || '', a.data_nascimento || a.birthDate || null,
|
||
a.cpf || '', a.rg || '', a.rg_data_emissao || a.rgIssueDate || null,
|
||
a.nome_responsavel || a.guardianName || '', a.telefone_responsavel || a.guardianPhone || '',
|
||
a.cpf_responsavel || a.guardianCpf || '', a.data_nascimento_responsavel || a.guardianBirthDate || null,
|
||
a.turma_id || a.classId || null, a.status || 'active', a.data_matricula || a.registrationDate || null,
|
||
a.foto_url || a.photo || '', a.cep || a.addressZip || '', a.rua || a.addressStreet || '',
|
||
a.numero || a.addressNumber || '', a.bairro || a.addressNeighborhood || '', a.cidade || a.addressCity || '',
|
||
a.estado || a.addressState || '', a.desconto || a.discount || 0, a.tem_responsavel !== undefined ? a.tem_responsavel : (a.hasGuardian || false),
|
||
a.modelo_contrato_id || a.contractTemplateId || null, a.numero_matricula || a.enrollmentNumber || null,
|
||
a.senha_portal || a.portalPassword || null, a.motivo_cancelamento || a.cancellationReason || null,
|
||
a.faceDescriptor ? JSON.stringify(a.faceDescriptor) : null
|
||
]
|
||
);
|
||
return result.rows[0];
|
||
}
|
||
|
||
export async function updateAluno(id, a) {
|
||
const result = await pool.query(
|
||
`UPDATE alunos SET
|
||
nome=$1, email=$2, telefone=$3, data_nascimento=$4, cpf=$5, rg=$6, rg_data_emissao=$7,
|
||
nome_responsavel=$8, telefone_responsavel=$9, cpf_responsavel=$10, data_nascimento_responsavel=$11,
|
||
turma_id=$12, status=$13, data_matricula=$14, foto_url=$15, cep=$16, rua=$17, numero=$18, bairro=$19, cidade=$20, estado=$21,
|
||
desconto=$22, tem_responsavel=$23, modelo_contrato_id=$24, numero_matricula=$25, senha_portal=$26,
|
||
motivo_cancelamento=$27, face_descriptor=COALESCE($28, face_descriptor)
|
||
WHERE id = $29 RETURNING *`,
|
||
[
|
||
a.nome || a.name, a.email || '', a.telefone || a.phone || '', a.data_nascimento || a.birthDate || null,
|
||
a.cpf || '', a.rg || '', a.rg_data_emissao || a.rgIssueDate || null,
|
||
a.nome_responsavel || a.guardianName || '', a.telefone_responsavel || a.guardianPhone || '',
|
||
a.cpf_responsavel || a.guardianCpf || '', a.data_nascimento_responsavel || a.guardianBirthDate || null,
|
||
a.turma_id || a.classId || null, a.status || 'active', a.data_matricula || a.registrationDate || null,
|
||
a.foto_url || a.photo || '', a.cep || a.addressZip || '', a.rua || a.addressStreet || '',
|
||
a.numero || a.addressNumber || '', a.bairro || a.addressNeighborhood || '', a.cidade || a.addressCity || '',
|
||
a.estado || a.addressState || '', a.desconto || a.discount || 0, a.tem_responsavel !== undefined ? a.tem_responsavel : (a.hasGuardian || false),
|
||
a.modelo_contrato_id || a.contractTemplateId || null, a.numero_matricula || a.enrollmentNumber || null,
|
||
a.senha_portal || a.portalPassword || null, a.motivo_cancelamento || a.cancellationReason || null,
|
||
a.faceDescriptor ? JSON.stringify(a.faceDescriptor) : null,
|
||
id
|
||
]
|
||
);
|
||
return result.rows[0];
|
||
}
|
||
|
||
export async function deleteAluno(id) {
|
||
await pool.query('DELETE FROM alunos WHERE id = $1', [id]);
|
||
}
|
||
|
||
// ============================================================
|
||
// CONTRATOS E MODELOS
|
||
// ============================================================
|
||
export async function getModelosContrato() {
|
||
const { rows } = await pool.query('SELECT * FROM modelos_contrato ORDER BY nome ASC');
|
||
return rows.map(r => ({ id: r.id, name: r.nome, content: r.conteudo }));
|
||
}
|
||
|
||
export async function insertModeloContrato(m) {
|
||
await pool.query('INSERT INTO modelos_contrato (id, nome, conteudo) VALUES ($1, $2, $3)', [m.id, m.name, m.content]);
|
||
}
|
||
|
||
export async function updateModeloContrato(id, m) {
|
||
await pool.query('UPDATE modelos_contrato SET nome=$1, conteudo=$2 WHERE id=$3', [m.name, m.content, id]);
|
||
}
|
||
|
||
export async function deleteModeloContrato(id) {
|
||
await pool.query('DELETE FROM modelos_contrato WHERE id=$1', [id]);
|
||
}
|
||
|
||
export async function getContratos() {
|
||
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) {
|
||
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) {
|
||
await pool.query('DELETE FROM contratos WHERE id=$1', [id]);
|
||
}
|
||
|
||
// ============================================================
|
||
// AULAS E CRONOGRAMA
|
||
// ============================================================
|
||
export async function getAulasByTurma(turma_id) {
|
||
const result = await pool.query(
|
||
`SELECT *, TO_CHAR(data, 'YYYY-MM-DD') as data_formatada FROM aulas WHERE turma_id = $1 ORDER BY data ASC, horario_inicio ASC`,
|
||
[turma_id]
|
||
);
|
||
return result.rows.map(row => ({
|
||
id: row.id,
|
||
classId: row.turma_id,
|
||
date: row.data_formatada,
|
||
startTime: row.horario_inicio,
|
||
endTime: row.horario_fim,
|
||
status: row.status,
|
||
type: row.tipo,
|
||
cancellationReason: row.motivo_cancelamento,
|
||
originalLessonId: row.aula_original_id
|
||
}));
|
||
}
|
||
|
||
export async function getAllAulas() {
|
||
const result = await pool.query(
|
||
`SELECT *, TO_CHAR(data, 'YYYY-MM-DD') as data_formatada FROM aulas ORDER BY data ASC, horario_inicio ASC`
|
||
);
|
||
return result.rows.map(row => ({
|
||
id: row.id,
|
||
classId: row.turma_id,
|
||
date: row.data_formatada,
|
||
startTime: row.horario_inicio,
|
||
endTime: row.horario_fim,
|
||
status: row.status,
|
||
type: row.tipo,
|
||
cancellationReason: row.motivo_cancelamento,
|
||
originalLessonId: row.aula_original_id
|
||
}));
|
||
}
|
||
|
||
export async function insertAulas(aulas) {
|
||
const client = await pool.connect();
|
||
try {
|
||
await client.query('BEGIN');
|
||
for (const a of aulas) {
|
||
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 UPDATE SET
|
||
turma_id=$2, data=$3, horario_inicio=$4, horario_fim=$5, status=$6, tipo=$7, motivo_cancelamento=$8, aula_original_id=$9`,
|
||
[
|
||
a.id, a.classId, a.date, a.startTime, a.endTime, a.status || 'scheduled', a.type || 'regular',
|
||
a.cancellationReason || null, a.originalLessonId || null
|
||
]
|
||
);
|
||
}
|
||
await client.query('COMMIT');
|
||
} catch (e) {
|
||
await client.query('ROLLBACK');
|
||
throw e;
|
||
} finally {
|
||
client.release();
|
||
}
|
||
}
|
||
|
||
export async function deleteAulas(ids) {
|
||
if (!ids || ids.length === 0) return;
|
||
await pool.query('DELETE FROM aulas WHERE id = ANY($1)', [ids]);
|
||
}
|
||
|
||
// ============================================================
|
||
// FREQUÊNCIAS (CHAMADA)
|
||
// ============================================================
|
||
export async function getFrequencias() {
|
||
const { rows } = await pool.query(`
|
||
SELECT *, TO_CHAR(data, 'YYYY-MM-DD"T"HH24:MI:SS') as formatted_data
|
||
FROM frequencias ORDER BY created_at DESC
|
||
`);
|
||
return rows.map(r => ({
|
||
id: r.id,
|
||
studentId: r.aluno_id,
|
||
classId: r.turma_id,
|
||
lessonId: r.aula_id,
|
||
date: r.formatted_data || r.data,
|
||
photo: r.foto_url || r.foto,
|
||
verified: r.verificado,
|
||
type: r.tipo,
|
||
justification: r.justificativa,
|
||
justificationAccepted: r.justificativa_aceita,
|
||
createdAt: r.created_at
|
||
}));
|
||
}
|
||
|
||
export async function insertFrequencia(f) {
|
||
await pool.query(
|
||
`INSERT INTO frequencias (id, aluno_id, turma_id, aula_id, data, foto, verificado, tipo, justificativa, justificativa_aceita)
|
||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||
[
|
||
f.id, f.studentId, f.classId, f.lessonId, f.date, f.photo,
|
||
f.verified || false, f.type || 'presence', f.justification, f.justificationAccepted || false
|
||
]
|
||
);
|
||
}
|
||
|
||
export async function updateFrequencia(id, f) {
|
||
// Update apenas dos campos que podem mudar
|
||
await pool.query(
|
||
`UPDATE frequencias
|
||
SET tipo = $1, justificativa = $2, justificativa_aceita = $3, verificado = $4
|
||
WHERE id = $5`,
|
||
[f.type, f.justification, f.justificationAccepted, f.verified, id]
|
||
);
|
||
}
|
||
|
||
export async function deleteFrequencia(id) {
|
||
await pool.query('DELETE FROM frequencias WHERE id = $1', [id]);
|
||
}
|
||
|
||
// ============================================================
|
||
// PROVAS & QUESTÕES (FASE 5)
|
||
// ============================================================
|
||
export async function getProvas() {
|
||
const { rows: provasRows } = await pool.query('SELECT * FROM provas ORDER BY created_at DESC');
|
||
|
||
// Mapear campos para camelCase e buscar questoes para compatibilidade com o frontend antigo
|
||
const provasFormatadas = [];
|
||
for (const p of provasRows) {
|
||
const { rows: questoesRows } = await pool.query('SELECT * FROM questoes_provas WHERE prova_id = $1 ORDER BY ordem ASC', [p.id]);
|
||
|
||
provasFormatadas.push({
|
||
id: p.id,
|
||
classId: p.turma_id,
|
||
subjectId: p.disciplina_id,
|
||
periodId: p.periodo_id,
|
||
title: p.titulo,
|
||
durationMinutes: p.duracao_minutos,
|
||
status: p.status,
|
||
allowRetake: p.permitir_refacao,
|
||
isDeleted: p.is_deleted,
|
||
evaluationType: p.evaluation_type || 'exam',
|
||
questions: questoesRows.map(q => ({
|
||
id: q.id,
|
||
examId: q.prova_id,
|
||
text: q.texto,
|
||
options: q.opcoes || [],
|
||
correctAnswer: q.indice_correto,
|
||
order: q.ordem,
|
||
imageUrl: q.imagem_url
|
||
}))
|
||
});
|
||
}
|
||
return provasFormatadas;
|
||
}
|
||
|
||
export async function getQuestoesDaProva(provaId) {
|
||
const result = await pool.query('SELECT * FROM questoes_provas WHERE prova_id = $1 ORDER BY ordem ASC', [provaId]);
|
||
return result.rows;
|
||
}
|
||
|
||
export async function insertProva(p) {
|
||
await pool.query(
|
||
`INSERT INTO provas (id, turma_id, disciplina_id, periodo_id, titulo, duracao_minutos, status, permitir_refacao, is_deleted, evaluation_type)
|
||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||
[
|
||
p.id, p.turma_id || p.classId, p.disciplina_id || p.subjectId, p.periodo_id || p.periodId,
|
||
p.titulo || p.title, p.duracao_minutos || p.durationMinutes || 60, p.status || 'draft',
|
||
p.permitir_refacao || p.allowRetake || false,
|
||
p.is_deleted || p.isDeleted || false,
|
||
p.evaluation_type || p.evaluationType || 'exam'
|
||
]
|
||
);
|
||
}
|
||
|
||
export async function updateProva(id, p) {
|
||
await pool.query(
|
||
`UPDATE provas SET
|
||
turma_id = $1, disciplina_id = $2, periodo_id = $3, titulo = $4, duracao_minutos = $5, status = $6, permitir_refacao = $7, is_deleted = $8, evaluation_type = $9
|
||
WHERE id = $10`,
|
||
[
|
||
p.turma_id || p.classId, p.disciplina_id || p.subjectId, p.periodo_id || p.periodId,
|
||
p.titulo || p.title, p.duracao_minutos || p.durationMinutes || 60, p.status || 'draft',
|
||
p.permitir_refacao || p.allowRetake || false,
|
||
p.is_deleted || p.isDeleted || false,
|
||
p.evaluation_type || p.evaluationType || 'exam',
|
||
id
|
||
]
|
||
);
|
||
}
|
||
|
||
export async function deleteProva(id) {
|
||
await pool.query('DELETE FROM provas WHERE id = $1', [id]);
|
||
}
|
||
|
||
export async function syncQuestoesProva(provaId, questoes) {
|
||
const client = await pool.connect();
|
||
try {
|
||
await client.query('BEGIN');
|
||
await client.query('DELETE FROM questoes_provas WHERE prova_id = $1', [provaId]);
|
||
for (let i = 0; i < questoes.length; i++) {
|
||
const q = questoes[i];
|
||
await client.query(
|
||
`INSERT INTO questoes_provas (id, prova_id, texto, imagem_url, opcoes, indice_correto, ordem)
|
||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||
[q.id || require('crypto').randomUUID(), provaId, q.texto || q.text, q.imagem_url || q.imageUrl, JSON.stringify(q.opcoes || q.options || []), q.indice_correto ?? q.correctIndex ?? 0, i]
|
||
);
|
||
}
|
||
await client.query('COMMIT');
|
||
} catch (e) {
|
||
await client.query('ROLLBACK');
|
||
throw e;
|
||
} finally {
|
||
client.release();
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// SINCRONIZAÇÃO: JSON -> TABELAS RELACIONAIS
|
||
// Garante que IDs do JSON existam nas tabelas para evitar erro de Foreign Key
|
||
// ============================================================
|
||
export async function syncJsonToRelationalTables() {
|
||
const client = await pool.connect();
|
||
try {
|
||
const data = await getSchoolData();
|
||
if (!data) return;
|
||
|
||
console.log('[Sincronização] 🔄 Iniciando espelhamento TOTAL (Modo Blindado)...');
|
||
await client.query('BEGIN');
|
||
|
||
// Garantir colunas na tabela frequencias
|
||
await client.query(`ALTER TABLE frequencias ADD COLUMN IF NOT EXISTS justificativa TEXT`);
|
||
await client.query(`ALTER TABLE frequencias ADD COLUMN IF NOT EXISTS justificativa_aceita BOOLEAN DEFAULT FALSE`);
|
||
|
||
// 1. Sincronizar Cursos
|
||
if (data.courses && Array.isArray(data.courses)) {
|
||
const courseIds = data.courses.map(c => c.id).filter(Boolean);
|
||
if (courseIds.length > 0) {
|
||
await client.query('DELETE FROM cursos WHERE id != ALL($1)', [courseIds]);
|
||
} else {
|
||
await client.query('DELETE FROM cursos');
|
||
}
|
||
|
||
for (const c of data.courses) {
|
||
if (!c.id || !c.name) continue;
|
||
await client.query(
|
||
`INSERT INTO cursos (id, nome, duracao, duracao_meses, taxa_matricula, mensalidade, descricao, multa_percentual, juros_percentual)
|
||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||
ON CONFLICT (id) DO UPDATE SET
|
||
nome = EXCLUDED.nome, duracao = EXCLUDED.duracao, duracao_meses = EXCLUDED.duracao_meses,
|
||
taxa_matricula = EXCLUDED.taxa_matricula, mensalidade = EXCLUDED.mensalidade,
|
||
descricao = EXCLUDED.descricao, multa_percentual = EXCLUDED.multa_percentual,
|
||
juros_percentual = EXCLUDED.juros_percentual`,
|
||
[c.id, c.name, c.duration || '', c.durationMonths || 0, c.registrationFee || 0, c.monthlyFee || 0, c.description || '', c.finePercentage || 0, c.interestPercentage || 0]
|
||
);
|
||
}
|
||
}
|
||
|
||
// 1.5 Sincronizar Categorias de Funcionários
|
||
if (data.employeeCategories && Array.isArray(data.employeeCategories)) {
|
||
for (const cat of data.employeeCategories) {
|
||
if (!cat.id || !cat.name) continue;
|
||
await client.query(
|
||
`INSERT INTO categorias_funcionarios (id, nome) VALUES ($1, $2)
|
||
ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome`,
|
||
[cat.id, cat.name]
|
||
);
|
||
}
|
||
}
|
||
|
||
// 1.6 Sincronizar Funcionários
|
||
if (data.employees && Array.isArray(data.employees)) {
|
||
for (const emp of data.employees) {
|
||
if (!emp.id || !emp.name) continue;
|
||
await client.query(
|
||
`INSERT INTO funcionarios (id, nome, cpf, email, telefone, categoria_id, data_admissao)
|
||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||
ON CONFLICT (id) DO UPDATE SET
|
||
nome = EXCLUDED.nome, cpf = EXCLUDED.cpf, email = EXCLUDED.email,
|
||
telefone = EXCLUDED.telefone, categoria_id = EXCLUDED.categoria_id,
|
||
data_admissao = EXCLUDED.data_admissao`,
|
||
[emp.id, emp.name, emp.cpf || '', emp.email || '', emp.phone || '', emp.categoryId || null, emp.admissionDate || null]
|
||
);
|
||
}
|
||
}
|
||
|
||
// Garantir colunas de refação e soft delete em provas
|
||
await client.query('ALTER TABLE provas ADD COLUMN IF NOT EXISTS permitir_refacao BOOLEAN DEFAULT FALSE');
|
||
await client.query('ALTER TABLE provas ADD COLUMN IF NOT EXISTS is_deleted BOOLEAN DEFAULT FALSE');
|
||
await client.query("ALTER TABLE provas ADD COLUMN IF NOT EXISTS evaluation_type VARCHAR(50) DEFAULT 'exam'");
|
||
|
||
// 2. Sincronizar Disciplinas (Subjects)
|
||
if (data.subjects && Array.isArray(data.subjects)) {
|
||
const subIds = data.subjects.map(s => s.id).filter(Boolean);
|
||
if (subIds.length > 0) {
|
||
await client.query('DELETE FROM disciplinas WHERE id != ALL($1)', [subIds]);
|
||
} else {
|
||
await client.query('DELETE FROM disciplinas');
|
||
}
|
||
|
||
for (const sub of data.subjects) {
|
||
if (!sub.id || !sub.name) continue;
|
||
await client.query(
|
||
`INSERT INTO disciplinas (id, nome)
|
||
VALUES ($1, $2)
|
||
ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome`,
|
||
[sub.id, sub.name]
|
||
);
|
||
}
|
||
}
|
||
|
||
// 0. Garantir esquema atualizado
|
||
await client.query('ALTER TABLE alunos_cobrancas ADD COLUMN IF NOT EXISTS valor_pago NUMERIC(10,2) DEFAULT 0');
|
||
await client.query('ALTER TABLE alunos_cobrancas ADD COLUMN IF NOT EXISTS local_id VARCHAR(255)');
|
||
await client.query('CREATE INDEX IF NOT EXISTS idx_cobrancas_local_id ON alunos_cobrancas(local_id)');
|
||
|
||
// 1. Sincronizar Perfil da Escola (Configurações)
|
||
if (data.periods && Array.isArray(data.periods)) {
|
||
const periodIds = data.periods.map(p => p.id).filter(Boolean);
|
||
if (periodIds.length > 0) {
|
||
await client.query('DELETE FROM periodos WHERE id != ALL($1)', [periodIds]);
|
||
} else {
|
||
await client.query('DELETE FROM periodos');
|
||
}
|
||
|
||
for (const p of data.periods) {
|
||
if (!p.id || !p.name) continue;
|
||
await client.query(
|
||
`INSERT INTO periodos (id, nome)
|
||
VALUES ($1, $2)
|
||
ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome`,
|
||
[p.id, p.name]
|
||
);
|
||
}
|
||
}
|
||
|
||
// 4. Sincronizar Turmas
|
||
if (data.classes && Array.isArray(data.classes)) {
|
||
const classIds = data.classes.map(t => t.id).filter(Boolean);
|
||
if (classIds.length > 0) {
|
||
await client.query('DELETE FROM turmas WHERE id != ALL($1)', [classIds]);
|
||
} else {
|
||
await client.query('DELETE FROM turmas');
|
||
}
|
||
|
||
for (const t of data.classes) {
|
||
if (!t.id || !t.name) continue;
|
||
await client.query(
|
||
`INSERT INTO turmas (id, nome, curso_id, professor, horario, dia_semana, max_alunos, data_inicio, data_fim, horario_inicio_padrao, horario_fim_padrao)
|
||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||
ON CONFLICT (id) DO UPDATE SET
|
||
nome = EXCLUDED.nome, curso_id = EXCLUDED.curso_id, professor = EXCLUDED.professor,
|
||
horario = EXCLUDED.horario, dia_semana = EXCLUDED.dia_semana, max_alunos = EXCLUDED.max_alunos,
|
||
data_inicio = EXCLUDED.data_inicio, data_fim = EXCLUDED.data_fim,
|
||
horario_inicio_padrao = EXCLUDED.horario_inicio_padrao, horario_fim_padrao = EXCLUDED.horario_fim_padrao`,
|
||
[t.id, t.name, t.courseId || null, t.teacher || '', t.schedule || '', t.scheduleDay || null, t.maxStudents || 30, t.startDate || null, t.endDate || null, t.defaultStartTime || null, t.defaultEndTime || null]
|
||
);
|
||
}
|
||
}
|
||
|
||
// 5. Sincronizar Alunos
|
||
if (data.students && Array.isArray(data.students)) {
|
||
const studentIds = data.students.map(s => s.id).filter(Boolean);
|
||
if (studentIds.length > 0) {
|
||
await client.query('DELETE FROM alunos WHERE id != ALL($1)', [studentIds]);
|
||
} else {
|
||
await client.query('DELETE FROM alunos');
|
||
}
|
||
|
||
for (const s of data.students) {
|
||
if (!s.id || !s.name) continue;
|
||
await client.query(
|
||
`INSERT INTO alunos (
|
||
id, nome, email, telefone, data_nascimento, cpf, rg, rg_data_emissao,
|
||
nome_responsavel, telefone_responsavel, cpf_responsavel, data_nascimento_responsavel,
|
||
turma_id, status, data_matricula, foto_url, cep, rua, numero, bairro, cidade, estado,
|
||
desconto, tem_responsavel, modelo_contrato_id, numero_matricula, senha_portal
|
||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27)
|
||
ON CONFLICT (id) DO UPDATE SET
|
||
nome = EXCLUDED.nome, email = EXCLUDED.email, telefone = EXCLUDED.telefone, data_nascimento = EXCLUDED.data_nascimento,
|
||
cpf = EXCLUDED.cpf, rg = EXCLUDED.rg, rg_data_emissao = EXCLUDED.rg_data_emissao,
|
||
nome_responsavel = EXCLUDED.nome_responsavel, telefone_responsavel = EXCLUDED.telefone_responsavel,
|
||
cpf_responsavel = EXCLUDED.cpf_responsavel, data_nascimento_responsavel = EXCLUDED.data_nascimento_responsavel,
|
||
turma_id = EXCLUDED.turma_id, status = EXCLUDED.status, data_matricula = EXCLUDED.data_matricula,
|
||
foto_url = EXCLUDED.foto_url, cep = EXCLUDED.cep, rua = EXCLUDED.rua, numero = EXCLUDED.numero,
|
||
bairro = EXCLUDED.bairro, cidade = EXCLUDED.cidade, estado = EXCLUDED.estado,
|
||
desconto = EXCLUDED.desconto, tem_responsavel = EXCLUDED.tem_responsavel,
|
||
modelo_contrato_id = EXCLUDED.modelo_contrato_id, numero_matricula = EXCLUDED.numero_matricula,
|
||
senha_portal = EXCLUDED.senha_portal`,
|
||
[
|
||
s.id, s.name, s.email || '', s.phone || '', s.birthDate || null, s.cpf || '', s.rg || '', s.rgIssueDate || null,
|
||
s.guardianName || '', s.guardianPhone || '', s.guardianCpf || '', s.guardianBirthDate || null,
|
||
s.classId || null, s.status || 'active', s.registrationDate || null, s.photo || '',
|
||
s.addressZip || '', s.addressStreet || '', s.addressNumber || '', s.addressNeighborhood || '', s.addressCity || '', s.addressState || '',
|
||
s.discount || 0, s.hasGuardian || false, s.contractTemplateId || null, s.enrollmentNumber || null, s.portalPassword || null
|
||
]
|
||
);
|
||
}
|
||
}
|
||
|
||
// 6. Sincronizar Provas
|
||
if (data.exams && Array.isArray(data.exams)) {
|
||
const examIds = data.exams.map(e => e.id).filter(Boolean);
|
||
if (examIds.length > 0) {
|
||
await client.query('DELETE FROM provas WHERE id != ALL($1)', [examIds]);
|
||
} else {
|
||
await client.query('DELETE FROM provas');
|
||
}
|
||
|
||
for (const e of data.exams) {
|
||
if (!e.id || !e.title) continue;
|
||
await client.query(
|
||
`INSERT INTO provas (id, turma_id, disciplina_id, periodo_id, titulo, duracao_minutos, status, permitir_refacao)
|
||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||
ON CONFLICT (id) DO UPDATE SET
|
||
turma_id = EXCLUDED.turma_id, disciplina_id = EXCLUDED.disciplina_id, periodo_id = EXCLUDED.periodo_id,
|
||
titulo = EXCLUDED.titulo, duracao_minutos = EXCLUDED.duracao_minutos, status = EXCLUDED.status,
|
||
permitir_refacao = EXCLUDED.permitir_refacao`,
|
||
[e.id, e.classId || null, e.subjectId || null, e.periodId || null, e.title, e.durationMinutes || 60, e.status || 'draft', e.allowRetake || false]
|
||
).catch(err => console.warn(`[Sync:Provas] Erro na prova ${e.id}:`, err.message));
|
||
}
|
||
}
|
||
|
||
// 7. Sincronizar Frequências
|
||
// [REMOVIDO] A tabela de frequencias agora é a Single Source of Truth (SQL-First).
|
||
// O JSON legado data.attendance é ignorado para não sobrescrever os registros reais do banco.
|
||
|
||
// 8. Sincronizar Cobranças (Financeiro) — com campos ricos para migração SQL-First
|
||
if (data.payments && Array.isArray(data.payments)) {
|
||
for (const p of data.payments) {
|
||
if (!p.studentId || !p.id) continue;
|
||
|
||
// Normalizar status para o padrão SQL (maiúsculas)
|
||
const rawStatus = (p.status || 'pending').toLowerCase();
|
||
const statusMap = { 'paid': 'PAGO', 'received': 'PAGO', 'confirmed': 'PAGO', 'overdue': 'ATRASADO', 'cancelled': 'CANCELADO' };
|
||
const sqlStatus = statusMap[rawStatus] || 'PENDENTE';
|
||
const isPaid = sqlStatus === 'PAGO';
|
||
const amount = Number(p.amount || 0);
|
||
const discount = Number(p.discount || 0);
|
||
|
||
// O valor da parcela (face value) é sempre o amount do JSON (ex: 170)
|
||
// O desconto é condicional e NÃO altera o valor base da parcela
|
||
let valorBruto = amount;
|
||
let valorPago = 0;
|
||
|
||
if (isPaid) {
|
||
// Usar valor_pago explícito do JSON se disponível, senão calcular (amount - discount)
|
||
valorPago = Number(p.valor_pago || 0) || (amount - discount);
|
||
}
|
||
|
||
if (p.asaasPaymentId) {
|
||
// Cobrança vinculada ao Asaas (usa ON CONFLICT em asaas_payment_id)
|
||
await client.query(
|
||
`INSERT INTO alunos_cobrancas (
|
||
local_id, aluno_id, asaas_payment_id, asaas_installment_id, installment,
|
||
valor, vencimento, link_boleto, status,
|
||
description, type, discount, installment_number, total_installments,
|
||
contract_id, asaas_payment_url, amount_original, data_pagamento, valor_pago
|
||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
||
ON CONFLICT (asaas_payment_id) DO UPDATE SET
|
||
local_id = COALESCE(EXCLUDED.local_id, alunos_cobrancas.local_id),
|
||
aluno_id = EXCLUDED.aluno_id,
|
||
asaas_installment_id = COALESCE(EXCLUDED.asaas_installment_id, alunos_cobrancas.asaas_installment_id),
|
||
installment = COALESCE(EXCLUDED.installment, alunos_cobrancas.installment),
|
||
valor = EXCLUDED.valor,
|
||
vencimento = EXCLUDED.vencimento,
|
||
link_boleto = COALESCE(EXCLUDED.link_boleto, alunos_cobrancas.link_boleto),
|
||
status = CASE WHEN alunos_cobrancas.status = 'PAGO' THEN alunos_cobrancas.status ELSE EXCLUDED.status END,
|
||
description = COALESCE(EXCLUDED.description, alunos_cobrancas.description),
|
||
type = COALESCE(EXCLUDED.type, alunos_cobrancas.type),
|
||
discount = COALESCE(EXCLUDED.discount, alunos_cobrancas.discount),
|
||
installment_number = COALESCE(EXCLUDED.installment_number, alunos_cobrancas.installment_number),
|
||
total_installments = COALESCE(EXCLUDED.total_installments, alunos_cobrancas.total_installments),
|
||
contract_id = COALESCE(EXCLUDED.contract_id, alunos_cobrancas.contract_id),
|
||
asaas_payment_url = COALESCE(EXCLUDED.asaas_payment_url, alunos_cobrancas.asaas_payment_url),
|
||
amount_original = COALESCE(EXCLUDED.amount_original, alunos_cobrancas.amount_original),
|
||
data_pagamento = COALESCE(EXCLUDED.data_pagamento, alunos_cobrancas.data_pagamento),
|
||
valor_pago = EXCLUDED.valor_pago`,
|
||
[
|
||
p.id,
|
||
p.studentId,
|
||
p.asaasPaymentId,
|
||
p.asaasInstallmentId || p.installmentId || null,
|
||
p.installment || null,
|
||
valorBruto,
|
||
p.dueDate,
|
||
p.bankSlipUrl || p.link || null,
|
||
sqlStatus,
|
||
p.description || null,
|
||
p.type || 'monthly',
|
||
discount,
|
||
p.installmentNumber || null,
|
||
p.totalInstallments || null,
|
||
p.contractId || null,
|
||
p.asaasPaymentUrl || null,
|
||
valorBruto,
|
||
p.paidDate || null,
|
||
valorPago
|
||
]
|
||
).catch(err => console.warn(`[Sync:Finance] Erro no boleto Asaas ${p.asaasPaymentId}:`, err.message));
|
||
} else {
|
||
// Cobrança manual (sem asaasPaymentId)
|
||
// Verificamos por local_id para evitar duplicação
|
||
const existing = await client.query('SELECT id FROM alunos_cobrancas WHERE local_id = $1', [p.id]);
|
||
if (existing.rows.length > 0) {
|
||
await client.query(
|
||
`UPDATE alunos_cobrancas SET
|
||
aluno_id = $1,
|
||
valor = $2,
|
||
vencimento = $3,
|
||
status = $4,
|
||
description = $5,
|
||
type = $6,
|
||
discount = $7,
|
||
installment_number = $8,
|
||
total_installments = $9,
|
||
contract_id = $10,
|
||
amount_original = $11,
|
||
data_pagamento = $12,
|
||
valor_pago = $13
|
||
WHERE local_id = $14`,
|
||
[
|
||
p.studentId,
|
||
valorBruto,
|
||
p.dueDate,
|
||
sqlStatus,
|
||
p.description || null,
|
||
p.type || 'monthly',
|
||
discount,
|
||
p.installmentNumber || null,
|
||
p.totalInstallments || null,
|
||
p.contractId || null,
|
||
valorBruto,
|
||
p.paidDate || null,
|
||
valorPago,
|
||
p.id
|
||
]
|
||
).catch(err => console.warn(`[Sync:Finance] Erro ao atualizar boleto manual ${p.id}:`, err.message));
|
||
} else {
|
||
await client.query(
|
||
`INSERT INTO alunos_cobrancas (
|
||
local_id, aluno_id, valor, vencimento, status,
|
||
description, type, discount, installment_number, total_installments,
|
||
contract_id, amount_original, data_pagamento, valor_pago
|
||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)`,
|
||
[
|
||
p.id,
|
||
p.studentId,
|
||
valorBruto,
|
||
p.dueDate,
|
||
sqlStatus,
|
||
p.description || null,
|
||
p.type || 'monthly',
|
||
discount,
|
||
p.installmentNumber || null,
|
||
p.totalInstallments || null,
|
||
p.contractId || null,
|
||
valorBruto,
|
||
p.paidDate || null,
|
||
valorPago
|
||
]
|
||
).catch(err => console.warn(`[Sync:Finance] Erro ao inserir boleto manual ${p.id}:`, err.message));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
await client.query('COMMIT');
|
||
console.log('[Sincronização] 🚀 Espelhamento TOTAL concluído com sucesso!');
|
||
} catch (err) {
|
||
await client.query('ROLLBACK');
|
||
console.error('[Sincronização] ❌ Erro Crítico:', err.message);
|
||
} finally {
|
||
client.release();
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// EXPORT POOL para queries diretas quando necessário
|
||
// ============================================================
|
||
export { pool };
|
||
export default pool;
|