fix: biometric attendance timezone shift and mandatory lesson binding
This commit is contained in:
parent
1552e5cb19
commit
a1b5075e41
|
|
@ -231,11 +231,41 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ data, updateData
|
||||||
return;
|
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 = {
|
const newAttendance: Attendance = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
studentId: detectedStudentId,
|
studentId: detectedStudentId,
|
||||||
classId: detectedClassId,
|
classId: detectedClassId,
|
||||||
date: new Date().toISOString(),
|
lessonId: activeLesson.id, // Vínculo obrigatório agora
|
||||||
|
date: localDateStr,
|
||||||
photo: capturedImage,
|
photo: capturedImage,
|
||||||
type: 'presence',
|
type: 'presence',
|
||||||
verified: true
|
verified: true
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,18 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
|
||||||
const [showAbsenceModal, setShowAbsenceModal] = useState(false);
|
const [showAbsenceModal, setShowAbsenceModal] = useState(false);
|
||||||
const [isClosing, setIsClosing] = useState(false);
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
const [isClosing2, setIsClosing2] = 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 [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
||||||
|
|
||||||
const [absenceStudentId, setAbsenceStudentId] = useState('');
|
const [absenceStudentId, setAbsenceStudentId] = useState('');
|
||||||
const [absenceJustification, setAbsenceJustification] = 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 [absenceLessonId, setAbsenceLessonId] = useState('');
|
||||||
const [viewingAttachment, setViewingAttachment] = useState<string | null>(null);
|
const [viewingAttachment, setViewingAttachment] = useState<string | null>(null);
|
||||||
const [attendanceForAttachment, setAttendanceForAttachment] = useState<Attendance | null>(null);
|
const [attendanceForAttachment, setAttendanceForAttachment] = useState<Attendance | null>(null);
|
||||||
|
|
@ -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 lesson = data.lessons.find(l => l.id === record.id.replace('v-', ''));
|
||||||
const newType = (record.type === 'absence' || record.type === 'awaiting') ? 'presence' : 'absence';
|
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 = {
|
const newRecord: Attendance = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
studentId: record.studentId,
|
studentId: record.studentId,
|
||||||
classId: record.classId,
|
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,
|
verified: true,
|
||||||
type: newType,
|
type: newType,
|
||||||
...(lesson ? { lessonId: lesson.id } : {}) // Vinculação rígida para não haver duplicidade
|
...(lesson ? { lessonId: lesson.id } : {}) // Vinculação rígida para não haver duplicidade
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,8 @@ export interface Attendance {
|
||||||
id: string;
|
id: string;
|
||||||
studentId: string;
|
studentId: string;
|
||||||
classId: string;
|
classId: string;
|
||||||
date: string; // ISO String
|
lessonId?: string;
|
||||||
|
date: string; // ISO String ou Local ISO
|
||||||
photo?: string; // Base64 (Optional for absences)
|
photo?: string; // Base64 (Optional for absences)
|
||||||
verified: boolean;
|
verified: boolean;
|
||||||
type?: 'presence' | 'absence';
|
type?: 'presence' | 'absence';
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,8 @@ export interface Attendance {
|
||||||
id: string;
|
id: string;
|
||||||
studentId: string;
|
studentId: string;
|
||||||
classId: string;
|
classId: string;
|
||||||
date: string; // ISO String (UTC)
|
lessonId?: string;
|
||||||
|
date: string; // ISO String (UTC) ou Local ISO
|
||||||
photo?: string;
|
photo?: string;
|
||||||
verified: boolean;
|
verified: boolean;
|
||||||
type?: 'presence' | 'absence';
|
type?: 'presence' | 'absence';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue