fix(attendance): corrige timezone shift (Rule 25), priorizacao de justificativas sobre faltas vazias e resolve duplicatas no toggle do manager
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m10s Details

This commit is contained in:
Sidney 2026-05-30 21:17:43 -03:00
parent 18c635d72a
commit 1d56f91d5a
4 changed files with 38 additions and 13 deletions

View File

@ -146,11 +146,31 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
} else {
// Toggle existing record
const newType = record.type === 'absence' ? 'presence' : 'absence';
const modifiedRecord = { ...record, type: newType, justification: undefined, justificationAccepted: undefined };
updatedAttendance = updatedAttendance.map(a =>
a.id === record.id ? modifiedRecord : a
);
await fetch(`/api/frequencias/${record.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(modifiedRecord) });
// Encontrar TODOS os registros duplicados para a mesma aula e aluno
const studentId = record.studentId;
const lessonId = (record as any).lessonId;
// Determina quais registros modificar (por lessonId ou por horário se não tiver lessonId)
const matchingIds = updatedAttendance
.filter(a => a.studentId === studentId && ((a as any).lessonId === lessonId || (!lessonId && a.date === record.date)))
.map(a => a.id);
// Atualiza todos na memória para refletir a mudança instantaneamente
matchingIds.forEach(id => {
const r = updatedAttendance.find(a => a.id === id);
if (r) {
const modifiedRecord = { ...r, type: newType, justification: null, justificationAccepted: null };
updatedAttendance = updatedAttendance.map(a => a.id === id ? modifiedRecord : a);
// Dispara a atualização silenciosa para o backend de cada duplicata
fetch(`/api/frequencias/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(modifiedRecord)
}).catch(console.error);
}
});
}
setDbAttendance(updatedAttendance);
@ -309,6 +329,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
const matchedRecord = matchingRecords.find(a => a.type === 'presence' || !a.type) ||
matchingRecords.find(a => a.type === 'absence' && a.justificationAccepted) ||
matchingRecords.find(a => a.type === 'absence' && a.justification) ||
matchingRecords[0];
if (matchedRecord) {
@ -489,6 +510,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
const matchedRecord = matchingRecords.find(a => a.type === 'presence' || !a.type) ||
matchingRecords.find(a => a.type === 'absence' && a.justificationAccepted) ||
matchingRecords.find(a => a.type === 'absence' && a.justification) ||
matchingRecords[0];
if (matchedRecord) {
@ -607,6 +629,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
let record = matchingRecords.find(a => a.type === 'presence' || !a.type) ||
matchingRecords.find(a => a.type === 'absence' && a.justificationAccepted) ||
matchingRecords.find(a => a.type === 'absence' && a.justification) ||
matchingRecords[0];
if (!record && now >= lessonStart) {

View File

@ -807,7 +807,7 @@ export async function deleteAulas(ids) {
// ============================================================
export async function getFrequencias() {
const { rows } = await pool.query(`
SELECT *, TO_CHAR(data, 'YYYY-MM-DD"T"HH24:MI:SS') as formatted_data
SELECT *, TO_CHAR(data AT TIME ZONE 'America/Sao_Paulo', 'YYYY-MM-DD"T"HH24:MI:SS') as formatted_data
FROM frequencias ORDER BY created_at DESC
`);
return rows.map(r => ({
@ -837,12 +837,12 @@ export async function insertFrequencia(f) {
}
export async function updateFrequencia(id, f) {
// Update apenas dos campos que podem mudar
// Update apenas dos campos que podem mudar, mapeando undefined para null
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]
[f.type, f.justification !== undefined ? f.justification : null, f.justificationAccepted !== undefined ? f.justificationAccepted : null, f.verified !== undefined ? f.verified : null, id]
);
}

View File

@ -537,11 +537,12 @@ app.get('/api/portal/notas', authMiddleware, async (req, res) => {
// GET /api/portal/frequencia (SQL-First — Leitura direta do PostgreSQL)
app.get('/api/portal/frequencia', authMiddleware, async (req, res) => {
try {
const { rows } = await pool.query(
`SELECT *, TO_CHAR(data, 'YYYY-MM-DD"T"HH24:MI:SS') as formatted_data
FROM frequencias WHERE aluno_id = $1 ORDER BY data DESC`,
[req.user.studentId]
);
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
FROM frequencias
WHERE aluno_id = $1
ORDER BY created_at DESC
`, [req.user.studentId]);
const attendance = rows.map(r => ({
id: r.id,

View File

@ -214,6 +214,7 @@ export default function Frequencia() {
let record = matchingRecords.find(a => a.type === 'presence' || (a.verified === true && a.type !== 'absence')) ||
matchingRecords.find(a => a.type === 'absence' && a.justificationAccepted) ||
matchingRecords.find(a => a.type === 'absence' && a.justification) ||
matchingRecords[0];
// Se não encontrou registro real, verificar se precisa de justificativa associada