From 6530a81ef6dcca01187fdd2732bb681b688da846 Mon Sep 17 00:00:00 2001 From: Sidney Date: Sat, 30 May 2026 22:00:27 -0300 Subject: [PATCH] fix(attendance/enrollment): resolve attendance shifts rendering presences as absences and finalize dynamic pre-matricula forms --- manager/components/Classes.tsx | 21 +++++++++++++++-- manager/components/Courses.tsx | 26 ++++++++++++++++++++- manager/components/PreMatricula.tsx | 35 +++++++++++++++++++++++++++-- manager/services/database.js | 2 +- portal/server.selfhosted.js | 2 +- 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/manager/components/Classes.tsx b/manager/components/Classes.tsx index 6fccc5a..7aca500 100644 --- a/manager/components/Classes.tsx +++ b/manager/components/Classes.tsx @@ -213,8 +213,17 @@ const Classes: React.FC = ({ data, updateData, onNavigateToClass } body: JSON.stringify({ aulas: classLessons }) }).catch(e => console.warn('Erro ao salvar aulas no SQL:', e)); } - updateData({ lessons: updatedLessons }); - dbService.saveData({ ...data, lessons: updatedLessons }); + + // Sincroniza também a lista global de turmas para o JSON não deletá-la na sync reversa + let updatedClasses = [...(data.classes || [])]; + if (editingClass) { + updatedClasses = updatedClasses.map(c => c.id === editingClass.id ? newClass : c); + } else { + updatedClasses.push(newClass); + } + + updateData({ classes: updatedClasses, lessons: updatedLessons }); + dbService.saveData({ ...data, classes: updatedClasses, lessons: updatedLessons }); await loadData(); closeModal(); @@ -250,6 +259,14 @@ const Classes: React.FC = ({ data, updateData, onNavigateToClass } try { const res = await fetch(`/api/turmas/${id}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete'); + + // Remove a turma e suas aulas associadas do estado global/JSON para manter sincronizado + const updatedClasses = (data.classes || []).filter(c => c.id !== id); + const updatedLessons = (data.lessons || []).filter(l => l.classId !== id); + + updateData({ classes: updatedClasses, lessons: updatedLessons }); + dbService.saveData({ ...data, classes: updatedClasses, lessons: updatedLessons }); + await loadData(); } catch (err) { showAlert('Erro', 'Ocorreu um erro ao deletar a turma.', 'error'); diff --git a/manager/components/Courses.tsx b/manager/components/Courses.tsx index 57f0ad1..67224c8 100644 --- a/manager/components/Courses.tsx +++ b/manager/components/Courses.tsx @@ -91,6 +91,12 @@ const Courses: React.FC = ({ data, updateData }) => { }; try { + const finalCourseId = editingCourse ? editingCourse.id : crypto.randomUUID(); + const finalCourse: Course = { + ...finalData, + id: finalCourseId + }; + if (editingCourse) { const res = await fetch(`/api/cursos/${editingCourse.id}`, { method: 'PUT', @@ -102,10 +108,22 @@ const Courses: React.FC = ({ data, updateData }) => { const res = await fetch('/api/cursos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ...payload, id: crypto.randomUUID() }) + body: JSON.stringify({ ...payload, id: finalCourseId }) }); if (!res.ok) throw new Error('Failed'); } + + // Sincroniza a lista global de cursos para o JSON + let updatedCourses = [...(data.courses || [])]; + if (editingCourse) { + updatedCourses = updatedCourses.map(c => c.id === editingCourse.id ? finalCourse : c); + } else { + updatedCourses.push(finalCourse); + } + + updateData({ courses: updatedCourses }); + dbService.saveData({ ...data, courses: updatedCourses }); + await loadData(); closeModal(); } catch (err) { @@ -152,6 +170,12 @@ const Courses: React.FC = ({ data, updateData }) => { try { const res = await fetch(`/api/cursos/${id}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete'); + + // Sincroniza a remoção do curso no estado global/JSON + const updatedCourses = (data.courses || []).filter(c => c.id !== id); + updateData({ courses: updatedCourses }); + dbService.saveData({ ...data, courses: updatedCourses }); + await loadData(); } catch (err) { showAlert('Erro', 'Ocorreu um erro ao excluir o curso.', 'error'); diff --git a/manager/components/PreMatricula.tsx b/manager/components/PreMatricula.tsx index bf2619f..ff63945 100644 --- a/manager/components/PreMatricula.tsx +++ b/manager/components/PreMatricula.tsx @@ -189,7 +189,12 @@ const PreMatricula: React.FC = ({ data, onConvert }) => { const rows = filtered.map(i => [ i.nome, i.email, i.telefone, i.turmaNome, i.status, new Date(i.createdAt).toLocaleDateString('pt-BR'), - ...Array.from(allKeys).map(k => (i.respostas || {})[k] || '') + ...Array.from(allKeys).map(k => { + const val = (i.respostas || {})[k] || ''; + return typeof val === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(val) + ? val.split('-').reverse().join('/') + : val; + }) ]); const csv = [headers.join(';'), ...rows.map(r => r.join(';'))].join('\n'); const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); @@ -813,7 +818,33 @@ const PreMatricula: React.FC = ({ data, onConvert }) => { 📄 Ver Documento ) : ( - {String(value)} + + {(() => { + if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value)) { + const dateFormatted = value.split('-').reverse().join('/'); + const isBirthDateField = label.toLowerCase().includes('nasc') || label.toLowerCase().includes('nascimento'); + if (isBirthDateField) { + const parts = value.split('-'); + const birthYear = parseInt(parts[0], 10); + const birthMonth = parseInt(parts[1], 10); + const birthDay = parseInt(parts[2], 10); + + // Forçando fuso brasileiro para calcular a idade exata hoje (Regra 45) + const todayStr = new Date().toLocaleString('en-US', { timeZone: 'America/Sao_Paulo' }); + const today = new Date(todayStr); + + let age = today.getFullYear() - birthYear; + const monthDiff = (today.getMonth() + 1) - birthMonth; + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDay)) { + age--; + } + return `${dateFormatted} (${age} anos)`; + } + return dateFormatted; + } + return String(value); + })()} + )} ); diff --git a/manager/services/database.js b/manager/services/database.js index 5acc238..8ceab7d 100644 --- a/manager/services/database.js +++ b/manager/services/database.js @@ -807,7 +807,7 @@ export async function deleteAulas(ids) { // ============================================================ export async function getFrequencias() { const { rows } = await pool.query(` - SELECT *, TO_CHAR(data AT TIME ZONE 'America/Sao_Paulo', 'YYYY-MM-DD"T"HH24:MI:SS') as formatted_data + 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 => ({ diff --git a/portal/server.selfhosted.js b/portal/server.selfhosted.js index 7fac2a9..fa10b97 100644 --- a/portal/server.selfhosted.js +++ b/portal/server.selfhosted.js @@ -538,7 +538,7 @@ app.get('/api/portal/notas', authMiddleware, async (req, res) => { app.get('/api/portal/frequencia', authMiddleware, async (req, res) => { try { const { rows } = await pool.query(` - SELECT *, TO_CHAR(data AT TIME ZONE 'America/Sao_Paulo', 'YYYY-MM-DD"T"HH24:MI:SS') as formatted_data + SELECT *, TO_CHAR(data, 'YYYY-MM-DD"T"HH24:MI:SS') as formatted_data FROM frequencias WHERE aluno_id = $1 ORDER BY created_at DESC