fix(attendance/enrollment): resolve attendance shifts rendering presences as absences and finalize dynamic pre-matricula forms
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m18s Details

This commit is contained in:
Sidney 2026-05-30 22:00:27 -03:00
parent 1d56f91d5a
commit 6530a81ef6
5 changed files with 79 additions and 7 deletions

View File

@ -213,8 +213,17 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
body: JSON.stringify({ aulas: classLessons }) body: JSON.stringify({ aulas: classLessons })
}).catch(e => console.warn('Erro ao salvar aulas no SQL:', e)); }).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(); await loadData();
closeModal(); closeModal();
@ -250,6 +259,14 @@ const Classes: React.FC<ClassesProps> = ({ data, updateData, onNavigateToClass }
try { try {
const res = await fetch(`/api/turmas/${id}`, { method: 'DELETE' }); const res = await fetch(`/api/turmas/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Failed to 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(); await loadData();
} catch (err) { } catch (err) {
showAlert('Erro', 'Ocorreu um erro ao deletar a turma.', 'error'); showAlert('Erro', 'Ocorreu um erro ao deletar a turma.', 'error');

View File

@ -91,6 +91,12 @@ const Courses: React.FC<CoursesProps> = ({ data, updateData }) => {
}; };
try { try {
const finalCourseId = editingCourse ? editingCourse.id : crypto.randomUUID();
const finalCourse: Course = {
...finalData,
id: finalCourseId
};
if (editingCourse) { if (editingCourse) {
const res = await fetch(`/api/cursos/${editingCourse.id}`, { const res = await fetch(`/api/cursos/${editingCourse.id}`, {
method: 'PUT', method: 'PUT',
@ -102,10 +108,22 @@ const Courses: React.FC<CoursesProps> = ({ data, updateData }) => {
const res = await fetch('/api/cursos', { const res = await fetch('/api/cursos', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, 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'); 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(); await loadData();
closeModal(); closeModal();
} catch (err) { } catch (err) {
@ -152,6 +170,12 @@ const Courses: React.FC<CoursesProps> = ({ data, updateData }) => {
try { try {
const res = await fetch(`/api/cursos/${id}`, { method: 'DELETE' }); const res = await fetch(`/api/cursos/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Failed to 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(); await loadData();
} catch (err) { } catch (err) {
showAlert('Erro', 'Ocorreu um erro ao excluir o curso.', 'error'); showAlert('Erro', 'Ocorreu um erro ao excluir o curso.', 'error');

View File

@ -189,7 +189,12 @@ const PreMatricula: React.FC<Props> = ({ data, onConvert }) => {
const rows = filtered.map(i => [ const rows = filtered.map(i => [
i.nome, i.email, i.telefone, i.turmaNome, i.status, i.nome, i.email, i.telefone, i.turmaNome, i.status,
new Date(i.createdAt).toLocaleDateString('pt-BR'), 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 csv = [headers.join(';'), ...rows.map(r => r.join(';'))].join('\n');
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
@ -813,7 +818,33 @@ const PreMatricula: React.FC<Props> = ({ data, onConvert }) => {
📄 Ver Documento 📄 Ver Documento
</a> </a>
) : ( ) : (
<span className="text-slate-800 font-semibold truncate">{String(value)}</span> <span className="text-slate-800 font-semibold truncate">
{(() => {
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);
})()}
</span>
)} )}
</div> </div>
); );

View File

@ -807,7 +807,7 @@ export async function deleteAulas(ids) {
// ============================================================ // ============================================================
export async function getFrequencias() { export async function getFrequencias() {
const { rows } = await pool.query(` 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 FROM frequencias ORDER BY created_at DESC
`); `);
return rows.map(r => ({ return rows.map(r => ({

View File

@ -538,7 +538,7 @@ app.get('/api/portal/notas', authMiddleware, async (req, res) => {
app.get('/api/portal/frequencia', authMiddleware, async (req, res) => { app.get('/api/portal/frequencia', authMiddleware, async (req, res) => {
try { try {
const { rows } = await pool.query(` 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 FROM frequencias
WHERE aluno_id = $1 WHERE aluno_id = $1
ORDER BY created_at DESC ORDER BY created_at DESC