fix: biometric attendance timezone shift and mandatory lesson binding

This commit is contained in:
Sidney 2026-05-11 21:43:31 -03:00
parent 1552e5cb19
commit a1b5075e41
4 changed files with 146 additions and 100 deletions

View File

@ -231,11 +231,41 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ data, updateData
return;
}
// Encontrar a aula ativa para esta turma/aluno no momento da captura
const nowLocal = new Date();
const activeLesson = (data.lessons || []).find(l => {
if (l.classId !== detectedClassId || l.status === 'cancelled') return false;
const lessonDate = l.date; // YYYY-MM-DD
const startStr = `${lessonDate}T${l.startTime || '00:00'}:00`;
const endStr = `${lessonDate}T${l.endTime || '23:59'}:00`;
const lessonStart = new Date(startStr);
const lessonEnd = new Date(endStr);
// Janela: 30 min antes do início até o fim da aula
const windowStart = new Date(lessonStart.getTime() - 30 * 60 * 1000);
return nowLocal >= windowStart && nowLocal <= lessonEnd;
});
// REGRA ESTRITA: A presença só pode ser marcada se houver uma aula ativa
if (!activeLesson) {
showAlert('Atenção', "Nenhuma aula ativa detectada para esta turma no momento. A presença só pode ser registrada a partir de 30 minutos antes do início até o término da aula.", 'warning');
cancelCapture();
return;
}
// Gerar string de data local (YYYY-MM-DDTHH:MM:SS) sem fuso UTC para evitar o bug do dia seguinte
const localDateStr = nowLocal.getFullYear() + '-' +
String(nowLocal.getMonth() + 1).padStart(2, '0') + '-' +
String(nowLocal.getDate()).padStart(2, '0') + 'T' +
String(nowLocal.getHours()).padStart(2, '0') + ':' +
String(nowLocal.getMinutes()).padStart(2, '0') + ':' +
String(nowLocal.getSeconds()).padStart(2, '0');
const newAttendance: Attendance = {
id: crypto.randomUUID(),
studentId: detectedStudentId,
classId: detectedClassId,
date: new Date().toISOString(),
lessonId: activeLesson.id, // Vínculo obrigatório agora
date: localDateStr,
photo: capturedImage,
type: 'presence',
verified: true

View File

@ -39,12 +39,18 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
const [showAbsenceModal, setShowAbsenceModal] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const [isClosing2, setIsClosing2] = useState(false);
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
// Função para obter data local YYYY-MM-DD sem risco de fuso horário UTC
const getTodayLocal = () => {
const d = new Date();
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
};
const [selectedDate, setSelectedDate] = useState(getTodayLocal());
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
const [absenceStudentId, setAbsenceStudentId] = useState('');
const [absenceJustification, setAbsenceJustification] = useState('');
const [absenceDate, setAbsenceDate] = useState(new Date().toISOString().split('T')[0]);
const [absenceDate, setAbsenceDate] = useState(getTodayLocal());
const [absenceLessonId, setAbsenceLessonId] = useState('');
const [viewingAttachment, setViewingAttachment] = useState<string | null>(null);
const [attendanceForAttachment, setAttendanceForAttachment] = useState<Attendance | null>(null);
@ -61,7 +67,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
try {
const match = url.match(/^https?:\/\/[^\/]+\/(.+)$/);
if (match) return `/storage/${match[1]}`;
} catch(e) {}
} catch (e) { }
return url;
};
@ -74,11 +80,19 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
const lesson = data.lessons.find(l => l.id === record.id.replace('v-', ''));
const newType = (record.type === 'absence' || record.type === 'awaiting') ? 'presence' : 'absence';
const now = new Date();
const localDate = now.getFullYear() + '-' +
String(now.getMonth() + 1).padStart(2, '0') + '-' +
String(now.getDate()).padStart(2, '0') + 'T' +
String(now.getHours()).padStart(2, '0') + ':' +
String(now.getMinutes()).padStart(2, '0') + ':' +
String(now.getSeconds()).padStart(2, '0');
const newRecord: Attendance = {
id: crypto.randomUUID(),
studentId: record.studentId,
classId: record.classId,
date: lesson ? `${lesson.date}T${lesson.startTime || '00:00'}:00` : new Date().toISOString(),
date: lesson ? `${lesson.date}T${lesson.startTime || '00:00'}:00` : localDate,
verified: true,
type: newType,
...(lesson ? { lessonId: lesson.id } : {}) // Vinculação rígida para não haver duplicidade
@ -126,7 +140,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
setViewingAttachment(null);
setAttendanceForAttachment(null);
showAlert('Sucesso', 'Arquivo removido com sucesso.', 'success');
} catch(e) {
} catch (e) {
console.error('Erro ao excluir anexo do registro', e);
}
};
@ -237,7 +251,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
try {
const parsed = JSON.parse(justMotivo);
justMotivo = parsed.motivo || justMotivo;
} catch(e) {}
} catch (e) { }
}
return [
@ -593,7 +607,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{tableRows.map(({lesson, record}) => {
{tableRows.map(({ lesson, record }) => {
const recordDate = new Date(record.date);
const time = recordDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
@ -604,7 +618,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
const parsed = JSON.parse(justMotivo);
justMotivo = parsed.motivo || justMotivo;
justAttachment = parsed.arquivo || parsed.arquivo_base64 || null;
} catch(e) {}
} catch (e) { }
}
const isAbsence = record.type === 'absence';

View File

@ -161,7 +161,8 @@ export interface Attendance {
id: string;
studentId: string;
classId: string;
date: string; // ISO String
lessonId?: string;
date: string; // ISO String ou Local ISO
photo?: string; // Base64 (Optional for absences)
verified: boolean;
type?: 'presence' | 'absence';

View File

@ -58,7 +58,8 @@ export interface Attendance {
id: string;
studentId: string;
classId: string;
date: string; // ISO String (UTC)
lessonId?: string;
date: string; // ISO String (UTC) ou Local ISO
photo?: string;
verified: boolean;
type?: 'presence' | 'absence';