fix: biometric attendance timezone shift and mandatory lesson binding
This commit is contained in:
parent
1552e5cb19
commit
a1b5075e41
|
|
@ -84,9 +84,9 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ data, updateData
|
|||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = stream;
|
||||
try {
|
||||
await videoRef.current.play();
|
||||
await videoRef.current.play();
|
||||
} catch (e) {
|
||||
console.error("Error playing video", e);
|
||||
console.error("Error playing video", e);
|
||||
}
|
||||
}
|
||||
setCameraActive(true);
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
@ -401,14 +415,14 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
|
|||
});
|
||||
|
||||
if (matchedRecord) {
|
||||
if (matchedRecord.type === 'absence') {
|
||||
if (matchedRecord.justificationAccepted) justified++;
|
||||
else absences++;
|
||||
} else if (matchedRecord.type === 'presence' || !matchedRecord.type) {
|
||||
presences++;
|
||||
}
|
||||
if (matchedRecord.type === 'absence') {
|
||||
if (matchedRecord.justificationAccepted) justified++;
|
||||
else absences++;
|
||||
} else if (matchedRecord.type === 'presence' || !matchedRecord.type) {
|
||||
presences++;
|
||||
}
|
||||
} else if (now > lessonEnd) {
|
||||
absences++;
|
||||
absences++;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -550,12 +564,12 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
|
|||
let justified = 0;
|
||||
|
||||
tableRows.forEach(row => {
|
||||
if (row.record.type === 'absence') {
|
||||
if (row.record.justificationAccepted) justified++;
|
||||
else absences++;
|
||||
} else if (row.record.type === 'presence' || (!row.record.type && !row.record.isVirtual)) {
|
||||
presences++;
|
||||
}
|
||||
if (row.record.type === 'absence') {
|
||||
if (row.record.justificationAccepted) justified++;
|
||||
else absences++;
|
||||
} else if (row.record.type === 'presence' || (!row.record.type && !row.record.isVirtual)) {
|
||||
presences++;
|
||||
}
|
||||
});
|
||||
|
||||
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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in New Issue