fix: strict attendance window and immediate cloud sync across manager and portal
This commit is contained in:
parent
a1b5075e41
commit
bfb2bc12db
|
|
@ -231,7 +231,7 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ data, updateData
|
|||
return;
|
||||
}
|
||||
|
||||
// Encontrar a aula ativa para esta turma/aluno no momento da captura
|
||||
// Encontrar a aula ativa para esta turma/aluno no momento exato da captura (Sem tolerância de 30 min)
|
||||
const nowLocal = new Date();
|
||||
const activeLesson = (data.lessons || []).find(l => {
|
||||
if (l.classId !== detectedClassId || l.status === 'cancelled') return false;
|
||||
|
|
@ -240,19 +240,19 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ data, updateData
|
|||
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 ESTREITA: Apenas do início ao fim da aula
|
||||
return nowLocal >= lessonStart && nowLocal <= lessonEnd;
|
||||
});
|
||||
|
||||
// REGRA ESTRITA: A presença só pode ser marcada se houver uma aula ativa
|
||||
// BLOQUEIO: Se não houver aula em andamento, impede o registro
|
||||
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');
|
||||
showAlert('Atenção', "Nenhuma aula em andamento para esta turma no momento. O registro biométrico só é permitido durante o horário oficial 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
|
||||
// Gerar string de data local para o banco de dados
|
||||
const localDateStr = nowLocal.getFullYear() + '-' +
|
||||
String(nowLocal.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(nowLocal.getDate()).padStart(2, '0') + 'T' +
|
||||
|
|
@ -264,7 +264,7 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ data, updateData
|
|||
id: crypto.randomUUID(),
|
||||
studentId: detectedStudentId,
|
||||
classId: detectedClassId,
|
||||
lessonId: activeLesson.id, // Vínculo obrigatório agora
|
||||
lessonId: activeLesson.id,
|
||||
date: localDateStr,
|
||||
photo: capturedImage,
|
||||
type: 'presence',
|
||||
|
|
@ -273,16 +273,20 @@ const AttendanceCapture: React.FC<AttendanceCaptureProps> = ({ data, updateData
|
|||
|
||||
const updatedAttendance = [...(data.attendance || []), newAttendance];
|
||||
updateData({ attendance: updatedAttendance });
|
||||
dbService.saveData({ ...data, attendance: updatedAttendance });
|
||||
|
||||
// Reset for next student
|
||||
// Sincronização em duas etapas: Local e Servidor (SQL)
|
||||
const updatedData = { ...data, attendance: updatedAttendance };
|
||||
dbService.saveData(updatedData);
|
||||
dbService.saveToCloud(updatedData);
|
||||
|
||||
// Reset de interface
|
||||
setCapturedImage(null);
|
||||
setShowConfirmModal(false);
|
||||
setDetectedStudentId(null);
|
||||
setDetectedClassId(null);
|
||||
setIsProcessing(false);
|
||||
closeModal();
|
||||
showAlert('Sucesso', "Presença confirmada com sucesso!", 'success');
|
||||
showAlert('Sucesso', "Presença registrada e sincronizada com o servidor!", 'success');
|
||||
};
|
||||
|
||||
const cancelCapture = () => {
|
||||
|
|
|
|||
|
|
@ -118,7 +118,9 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
|
|||
}
|
||||
|
||||
updateData({ attendance: updatedAttendance });
|
||||
dbService.saveData({ ...data, attendance: updatedAttendance });
|
||||
const updatedData = { ...data, attendance: updatedAttendance };
|
||||
dbService.saveData(updatedData);
|
||||
dbService.saveToCloud(updatedData); // Sincronia imediata com SQL
|
||||
showAlert('Sucesso', 'Status de frequência atualizado com sucesso.', 'success');
|
||||
};
|
||||
|
||||
|
|
@ -216,8 +218,10 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
|
|||
updatedAttendance.push(newAbsence);
|
||||
}
|
||||
|
||||
const updatedData = { ...data, attendance: updatedAttendance };
|
||||
updateData({ attendance: updatedAttendance });
|
||||
dbService.saveData({ ...data, attendance: updatedAttendance });
|
||||
dbService.saveData(updatedData);
|
||||
dbService.saveToCloud(updatedData); // Sincronia imediata com SQL
|
||||
|
||||
setAbsenceStudentId('');
|
||||
setAbsenceJustification('');
|
||||
|
|
@ -402,17 +406,16 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
|
|||
let justified = 0;
|
||||
const now = new Date();
|
||||
|
||||
deduplicatedLessons.forEach(lesson => {
|
||||
const lessonStart = new Date(lesson.date + 'T' + (lesson.startTime || '00:00') + ':00');
|
||||
const lessonEnd = new Date(lesson.date + 'T' + (lesson.endTime || '23:59') + ':00');
|
||||
const presenceStartWindow = new Date(lessonStart.getTime() - 30 * 60 * 1000);
|
||||
deduplicatedLessons.forEach(lesson => {
|
||||
const lessonStart = new Date(lesson.date + 'T' + (lesson.startTime || '00:00') + ':00');
|
||||
const lessonEnd = new Date(lesson.date + 'T' + (lesson.endTime || '23:59') + ':00');
|
||||
|
||||
const matchedRecord = studentActualRecords.find(a => {
|
||||
if ((a as any).lessonId === lesson.id) return true;
|
||||
if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true;
|
||||
const recordTime = new Date(a.date);
|
||||
return recordTime >= presenceStartWindow && recordTime <= lessonEnd;
|
||||
});
|
||||
const matchedRecord = studentActualRecords.find(a => {
|
||||
if ((a as any).lessonId === lesson.id) return true;
|
||||
if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true;
|
||||
const recordTime = new Date(a.date);
|
||||
return recordTime >= lessonStart && recordTime <= lessonEnd;
|
||||
});
|
||||
|
||||
if (matchedRecord) {
|
||||
if (matchedRecord.type === 'absence') {
|
||||
|
|
@ -520,16 +523,15 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
|
|||
deduplicatedLessons.forEach(lesson => {
|
||||
const lessonStart = new Date(lesson.date + 'T' + (lesson.startTime || '00:00') + ':00');
|
||||
const lessonEnd = new Date(lesson.date + 'T' + (lesson.endTime || '23:59') + ':00');
|
||||
const presenceStartWindow = new Date(lessonStart.getTime() - 30 * 60 * 1000); // 30 mins before
|
||||
|
||||
// Regra Estrita: Comparação exata com o horário da aula (sem 30 min de tolerância)
|
||||
let record = actualRecords.find(a => {
|
||||
if ((a as any).lessonId === lesson.id) return true;
|
||||
if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true;
|
||||
const recordTime = new Date(a.date);
|
||||
return recordTime >= presenceStartWindow && recordTime <= lessonEnd;
|
||||
return recordTime >= lessonStart && recordTime <= lessonEnd;
|
||||
});
|
||||
|
||||
if (!record && now >= presenceStartWindow) {
|
||||
if (!record && now >= lessonStart) {
|
||||
const isFinished = now > lessonEnd;
|
||||
record = {
|
||||
id: `v-${lesson.id}`,
|
||||
|
|
|
|||
|
|
@ -143,12 +143,11 @@ export default function Frequencia() {
|
|||
))
|
||||
);
|
||||
|
||||
// Merge and Categorize — Clone EXATO do Manager (AttendanceQuery.tsx)
|
||||
// Merge and Categorize — Clone EXATO do Manager (AttendanceQuery.tsx)
|
||||
const processedItems = deduplicatedLessons.map(lesson => {
|
||||
// Construir janela de tempo de forma resiliente
|
||||
const lessonStartMs = parseLessonDateTime(lesson.date, lesson.startTime || '00:00', 0);
|
||||
const lessonEndMs = parseLessonDateTime(lesson.date, lesson.endTime || '23:59', 23);
|
||||
const presenceStartWindowMs = lessonStartMs - (30 * 60 * 1000); // 30 mins before
|
||||
|
||||
// Buscar registro com a MESMA lógica do Manager (find, não filter)
|
||||
let record = attendance.find(a => {
|
||||
|
|
@ -157,19 +156,18 @@ export default function Frequencia() {
|
|||
if ((a as any).lessonId === lesson.id) return true;
|
||||
// 2. Match exato de string (formato do JSON/sync)
|
||||
if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true;
|
||||
// 3. Match por janela de tempo (30 min antes até fim da aula)
|
||||
// 3. Match por janela de tempo (Início ao Fim da aula - REGRA ESTREITA)
|
||||
const recordTimeMs = new Date(a.date).getTime();
|
||||
return recordTimeMs >= presenceStartWindowMs && recordTimeMs <= lessonEndMs;
|
||||
return recordTimeMs >= lessonStartMs && recordTimeMs <= lessonEndMs;
|
||||
});
|
||||
|
||||
// Se não encontrou registro real, verificar se precisa de justificativa associada
|
||||
// (pode existir um registro de justificativa com data ligeiramente diferente)
|
||||
if (!record) {
|
||||
record = attendance.find(a => {
|
||||
if (!a.date || typeof a.date !== 'string' || !a.justification) return false;
|
||||
if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true;
|
||||
const recordTimeMs = new Date(a.date).getTime();
|
||||
return recordTimeMs >= presenceStartWindowMs && recordTimeMs <= lessonEndMs;
|
||||
return recordTimeMs >= lessonStartMs && recordTimeMs <= lessonEndMs;
|
||||
}) || undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue