const { useState, useMemo, useEffect } = React; window.AdminAuditoria = function({ logs = [], globalUsers = [], absences = [], actions = [], setMapLogs, setPreviewImage, refreshData, settings = {}, isMaster = false, isReadOnly = false }) { const isGlobalAdmin = Boolean(isMaster || (window.SHIFT_CONFIG && window.SHIFT_CONFIG.isMaster)); const todayStr = window.getNICDate ? window.getNICDate() : new Date().toISOString().split('T')[0]; const [searchName, setSearchName] = useState(''); const [searchDate, setSearchDate] = useState(todayStr); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [selectedActionType, setSelectedActionType] = useState('ENTRADA_SALIDA'); const [activePreset, setActivePreset] = useState('today'); // 'today', 'this_week', 'this_quincena', 'this_month', 'custom' const [currentPage, setCurrentPage] = useState(1); const ITEMS_PER_PAGE = 50; const [editingLog, setEditingLog] = useState(null); const [showNewMarkModal, setShowNewMarkModal] = useState(false); const [formUser, setFormUser] = useState(''); const [formType, setFormType] = useState('entrada'); const [formDate, setFormDate] = useState(todayStr); const [formTime, setFormTime] = useState('08:00'); const [formJustification, setFormJustification] = useState(''); // Modal de edición rápida de justificación laboral const [justifyingLog, setJustifyingLog] = useState(null); const [selectedJustification, setSelectedJustification] = useState(''); const [customNote, setCustomNote] = useState(''); const [isSavingJustification, setIsSavingJustification] = useState(false); const [isAuditingClockouts, setIsAuditingClockouts] = useState(false); const [isSimulating, setIsSimulating] = useState(false); const [simulationReportModal, setSimulationReportModal] = useState(null); useEffect(() => { setCurrentPage(1); }, [searchName, searchDate, startDate, endDate, selectedActionType, activePreset]); const handleRun7DaySimulation = async () => { if (!confirm("¿Deseas ejecutar el Simulacro de Pruebas Automatizado de 7 Días?\n\nEste proceso simulará 7 días continuos de operación real para todos los colaboradores activos (Marcaciones GPS, tardanzas justificadas, horas extras autorizadas, permisos de tiempo compensado y turnos abiertos) para evaluar la estabilidad del sistema.")) { return; } setIsSimulating(true); try { const res = await window.apiCall('run_7day_simulation', {}); if (res && res.success) { setSimulationReportModal(res); if (refreshData) await refreshData(); } else { alert(res?.error || 'No se pudo completar el simulacro.'); } } catch (e) { alert('Error de conexión al ejecutar el simulacro.'); } finally { setIsSimulating(false); } }; const handleAuditMissingClockOuts = async () => { setIsAuditingClockouts(true); try { const res = await window.apiCall('audit_missing_clockouts', { date: searchDate || todayStr, start_date: startDate || searchDate || todayStr, end_date: endDate || searchDate || todayStr }); if (res && res.success) { const count = res.missing_clock_outs?.length || 0; if (count === 0) { alert(`Auditoría completada: No se detectaron turnos abiertos o salidas olvidadas para el período ${res.target_date}.`); } else { const names = res.missing_clock_outs.map(m => `• ${m.name} (Fecha: ${m.date || res.target_date}, Entrada: ${m.clock_in_time})`).join('\n'); alert(`Se detectaron ${count} turno(s) abierto(s) sin Salida:\n\n${names}\n\nSe enviaron ${res.alerts_dispatched} aviso(s) a los supervisores directos.`); } } else { alert(res?.error || 'No se pudo realizar la auditoría.'); } } catch (e) { alert('Error de conexión al auditar turnos abiertos.'); } finally { setIsAuditingClockouts(false); } }; // Helpers de Filtros Rápidos const applyQuickFilter = (type) => { setActivePreset(type); const [y, m, d] = todayStr.split('-').map(Number); if (type === 'today') { setSearchDate(todayStr); setStartDate(''); setEndDate(''); } else if (type === 'this_week') { const curr = new Date(); const dayOfWeek = curr.getDay(); const diffToMon = dayOfWeek === 0 ? -6 : 1 - dayOfWeek; const mon = new Date(curr.getFullYear(), curr.getMonth(), curr.getDate() + diffToMon); const sun = new Date(mon.getFullYear(), mon.getMonth(), mon.getDate() + 6); const fmt = (dt) => `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2,'0')}-${String(dt.getDate()).padStart(2,'0')}`; setSearchDate(''); setStartDate(fmt(mon)); setEndDate(fmt(sun)); } else if (type === 'this_quincena') { const startDay = d <= 15 ? '01' : '16'; const lastDayOfMonth = new Date(y, m, 0).getDate(); const endDay = d <= 15 ? '15' : String(lastDayOfMonth); const mm = String(m).padStart(2, '0'); setSearchDate(''); setStartDate(`${y}-${mm}-${startDay}`); setEndDate(`${y}-${mm}-${endDay}`); } else if (type === 'this_month') { const lastDayOfMonth = new Date(y, m, 0).getDate(); const mm = String(m).padStart(2, '0'); setSearchDate(''); setStartDate(`${y}-${mm}-01`); setEndDate(`${y}-${mm}-${lastDayOfMonth}`); } }; // Auditoría automática por registro (Puntualidad, Tardanza o Salida Temprana) const auditLogRecord = (log) => { if (log.justification) { const isExtra = log.justification.includes('Reingreso Extraordinario') || log.justification.includes('Día Libre') || log.justification.includes('Jornada en Día Libre'); if (isExtra) { return { status: 'info', text: log.justification, isAnomaly: false, isExtraordinary: true }; } return { status: 'justified', text: log.justification, isAnomaly: true }; } // Detección de Feriado de Empresa if (settings?.holidays && Array.isArray(settings.holidays)) { const hMatch = settings.holidays.find(h => h.date === log.date); if (hMatch) { return { status: 'info', text: `Día Feriado: ${hMatch.name}`, isAnomaly: false, isHoliday: true }; } } const user = (globalUsers || []).find(u => Number(u.id) === Number(log.userId)); if (!user) return { status: 'normal', text: 'Registro Normal', isAnomaly: false }; const dayCode = window.getDayOfWeekCode ? window.getDayOfWeekCode(log.date) : 'lun'; const customSchedule = user.scheduleCustom && user.scheduleCustom[dayCode]; const scheduledStart = customSchedule?.start || user.scheduleStart || '08:00'; const scheduledEnd = customSchedule?.end || user.scheduleEnd || '17:00'; const dateObj = new Date(Number(log.timestamp)); const hours = String(dateObj.getHours()).padStart(2, '0'); const minutes = String(dateObj.getMinutes()).padStart(2, '0'); const logTime = `${hours}:${minutes}`; const logTypeStr = String(log.type || '').toLowerCase(); // 1. Evaluación de Marcas de Almuerzo o Break (No son salidas de jornada laboral) if (logTypeStr.includes('almuerzo')) { if (logTypeStr.endsWith('_in') || logTypeStr.includes('inicio')) { return { status: 'normal', text: 'Inicio de Almuerzo', isAnomaly: false }; } if (logTypeStr.endsWith('_out') || logTypeStr.includes('fin')) { const almuerzoInLog = (logs || []).find(l => Number(l.userId) === Number(log.userId) && l.date === log.date && String(l.type).toLowerCase().includes('almuerzo') && (String(l.type).endsWith('_in') || String(l.type).includes('inicio')) ); if (almuerzoInLog) { const durMin = Math.round((Number(log.timestamp) - Number(almuerzoInLog.timestamp)) / 60000); const limitMin = settings?.lunchLimitMinutes || 60; if (durMin > limitMin + 5) { return { status: 'warning', text: `Exceso de Almuerzo (${durMin} min)`, isAnomaly: true, minutes: durMin - limitMin }; } } return { status: 'ok', text: 'Fin de Almuerzo (Puntual)', isAnomaly: false }; } return { status: 'normal', text: 'Registro de Almuerzo', isAnomaly: false }; } if (logTypeStr.includes('break') || logTypeStr.includes('descanso')) { if (logTypeStr.endsWith('_in') || logTypeStr.includes('inicio')) { return { status: 'normal', text: 'Inicio de Descanso', isAnomaly: false }; } return { status: 'ok', text: 'Fin de Descanso', isAnomaly: false }; } // 2. Evaluación de Entrada Trabajo if (log.type === 'entrada') { const [sh, sm] = scheduledStart.split(':').map(Number); const [lh, lm] = logTime.split(':').map(Number); const diffMin = (lh * 60 + lm) - (sh * 60 + sm); const grace = settings?.gracePeriodMinutes || 5; if (diffMin > grace) { return { status: 'warning', text: `Tardanza (+${diffMin} min)`, isAnomaly: true, anomalyType: 'tardanza', minutes: diffMin }; } return { status: 'ok', text: 'Entrada puntual', isAnomaly: false }; } // 3. Salida de Jornada Laboral const isWorkDeparture = log.type === 'salida' || log.type.startsWith('salida_') || log.type.includes('permiso_out'); if (isWorkDeparture) { const logMinutes = dateObj.getHours() * 60 + dateObj.getMinutes(); const [ehH, ehM] = scheduledEnd.split(':').map(Number); const scheduledEndMinutes = ehH * 60 + ehM; const minutesEarly = scheduledEndMinutes - logMinutes; if (minutesEarly > 5) { const hoursOut = (minutesEarly / 60).toFixed(1); if (minutesEarly >= 120 && minutesEarly <= 270) { return { status: 'warning', text: `Salida Consulta (${hoursOut} hrs fuera)`, isAnomaly: true, isMedicalAlert: true, minutes: minutesEarly }; } if (minutesEarly > 270) { return { status: 'danger', text: `Ausencia Crítica (${hoursOut} hrs fuera) - Evaluar Subsidio`, isAnomaly: true, isProlongedAbsence: true, minutes: minutesEarly }; } return { status: 'danger', text: `Salida anticipada (-${minutesEarly} min)`, isAnomaly: true, minutes: minutesEarly }; } return { status: 'ok', text: 'Salida regular completa', isAnomaly: false }; } return { status: 'normal', text: 'Normal', isAnomaly: false }; }; const formatLogType = (typeStr) => { if (!typeStr) return { label: 'MARCA', isStart: false, isEnd: false }; if (typeStr === 'entrada') return { label: 'Entrada Trabajo', isStart: true, isEnd: false }; if (typeStr === 'salida') return { label: 'Cierre de Día (Salida)', isStart: false, isEnd: true }; const isStart = typeStr.endsWith('_in'); const isEnd = typeStr.endsWith('_out') || typeStr.startsWith('vuelta_'); let cleanId = typeStr.replace('_in', '').replace('_out', '').replace('vuelta_', '').toLowerCase(); const matchedAction = (actions || []).find(a => (a.id || '').toLowerCase() === cleanId); let baseName = matchedAction ? matchedAction.label : cleanId.replace(/_\d+/g, '').replace(/_/g, ' ').toUpperCase(); let finalLabel = baseName; if (isStart) finalLabel = `Inicio ${baseName}`; else if (isEnd) finalLabel = `Fin ${baseName}`; return { label: finalLabel, isStart, isEnd }; }; const combinedAuditList = useMemo(() => { const mappedLogs = (logs || []).map(l => ({ ...l, isAbsenceRecord: false })); const mappedAbsences = (absences || []).map(a => ({ id: `abs_${a.id}`, realAbsenceId: a.id, userId: a.userId, userName: a.userName, type: a.type === 'injustificada' ? 'Falta Injustificada' : 'Falta Justificada', date: a.date, timestamp: a.createdAt || Date.now(), justification: a.notes ? `Observación: ${a.notes}` : 'Sin nota', isAbsenceRecord: true, absenceType: a.type })); return [...mappedLogs, ...mappedAbsences]; }, [logs, absences]); const filteredLogs = useMemo(() => { const result = combinedAuditList.filter(l => { const matchName = (l.userName || '').toLowerCase().includes(searchName.toLowerCase()); let matchDate = true; if (searchDate) { matchDate = l.date === searchDate; } else if (startDate && endDate) { matchDate = l.date >= startDate && l.date <= endDate; } let matchType = true; if (selectedActionType === 'ENTRADA_SALIDA') { matchType = l.type === 'entrada' || l.type === 'salida'; } else if (selectedActionType === 'ALMUERZO') { matchType = String(l.type).includes('almuerzo'); } else if (selectedActionType === 'AUSENCIAS') { matchType = l.isAbsenceRecord; } else if (selectedActionType === 'ALERTAS') { matchType = (l.justification && l.justification.startsWith('ALERTA:')) || window.isUserOffDay(globalUsers.find(u=>u.id===l.userId)?.scheduleDays, l.date); } else if (selectedActionType !== 'ALL') { matchType = String(l.type).includes(selectedActionType); } return matchName && matchDate && matchType; }); return result.sort((a,b) => { if (a.date !== b.date) return a.date > b.date ? -1 : 1; if (a.userName !== b.userName) return a.userName.localeCompare(b.userName); return a.timestamp - b.timestamp; }); }, [combinedAuditList, searchName, searchDate, startDate, endDate, selectedActionType, globalUsers]); const totalPages = Math.ceil(filteredLogs.length / ITEMS_PER_PAGE) || 1; const paginatedLogs = useMemo(() => { const startIdx = (currentPage - 1) * ITEMS_PER_PAGE; return filteredLogs.slice(startIdx, startIdx + ITEMS_PER_PAGE); }, [filteredLogs, currentPage]); const handleOpenLiveTrackingMap = () => { const todayNIC = window.getNICDate(); const activeTrackers = (globalUsers || []).filter(u => { if (!u.gpsTrackingEnabled || !u.lastLat || !u.lastLng || !u.lastGpsTime) return false; const signalDate = window.getNICDateFromObj(new Date(u.lastGpsTime)); return signalDate === todayNIC; }); if (activeTrackers.length === 0) { alert("No hay vendedores o colaboradores transmitiendo posición GPS en tiempo real en este momento."); return; } const mapItems = activeTrackers.map(u => ({ id: `live_${u.id}`, userName: `${u.name} (@${u.alias})`, type: 'visita_in', timestamp: u.lastGpsTime, location: { lat: u.lastLat, lng: u.lastLng }, justification: `Última transmisión en vivo: ${window.formatNICTimeFull(u.lastGpsTime)}` })); setMapLogs(mapItems); }; const handleOpenEdit = (log) => { setEditingLog(log); setFormUser(log.userId); setFormType(log.type); setFormDate(log.date); const dateObj = new Date(log.timestamp); const hours = String(dateObj.getHours()).padStart(2, '0'); const mins = String(dateObj.getMinutes()).padStart(2, '0'); setFormTime(`${hours}:${mins}`); setFormJustification(log.justification || ''); }; const handleSaveLogAudit = async (e) => { e.preventDefault(); const [h, m] = formTime.split(':').map(Number); const [y, mon, d] = formDate.split('-').map(Number); const targetTimestamp = new Date(y, mon - 1, d, h, m, 0).getTime(); const userObj = globalUsers.find(u => Number(u.id) === Number(formUser)); if (!userObj) return alert("Selecciona un colaborador."); const payload = { id: editingLog ? editingLog.id : null, userId: userObj.id, userName: userObj.name, type: formType, date: formDate, timestamp: targetTimestamp, justification: `${formJustification ? formJustification + ' | ' : ''}Ajuste Manual por Administrador (${new Date().toLocaleDateString()})` }; const res = await window.apiCall(editingLog ? 'update_log' : 'save_log', payload); if (res && res.success) { setEditingLog(null); setShowNewMarkModal(false); if (refreshData) await refreshData(); } else { alert("Error al procesar marca."); } }; const handleSaveJustification = async () => { if (!justifyingLog) return; setIsSavingJustification(true); const fullNote = customNote ? `${selectedJustification} (${customNote})` : selectedJustification; const normChoice = selectedJustification.toLowerCase(); // 1. Detección de descuento de vacaciones (Aplica para Tardanza y para Salida Temprana) if (normChoice.includes('vacacion') || normChoice.includes('medio')) { const isHalf = normChoice.includes('medio') || normChoice.includes('0.5'); const daysCount = isHalf ? 0.5 : 1.0; const incidenceType = justifyingLog.type === 'entrada' ? 'Entrada Tarde' : 'Salida Anticipada'; const resVac = await window.apiCall('deduct_vacation_from_audit', { userId: justifyingLog.userId, days: daysCount, date: justifyingLog.date, logId: justifyingLog.id, // <-- VÍNCULO DIRECTO reason: `Deducción por ${incidenceType} el ${window.formatDateToNIC(justifyingLog.date)}` }); if (!resVac || !resVac.success) { setIsSavingJustification(false); alert("Atención: No se pudo descontar del saldo de vacaciones. Detalle: " + (resVac?.error || "Error en el servidor")); return; } } // 2. Actualizar la marca en la tabla de logs const resLog = await window.apiCall('update_log', { id: justifyingLog.id, userId: justifyingLog.userId, type: justifyingLog.type, date: justifyingLog.date, timestamp: justifyingLog.timestamp, justification: fullNote }); setIsSavingJustification(false); if (resLog && resLog.success) { justifyingLog.justification = fullNote; setJustifyingLog(null); // Refrescar estado global del dashboard if (typeof refreshData === 'function') { await refreshData(); } } else { alert("Error al actualizar la marca: " + (resLog?.error || "Error de conexión")); } }; const handleDeleteLog = async (logId) => { if (!confirm("¿Eliminar este registro de marcaje de la auditoría?")) return; const res = await window.apiCall('delete_log', { id: logId }); if (res && res.success && refreshData) await refreshData(); }; // Generador de PDF Multi-Página Estructurado (Impresión Continua sin Truncamiento) const handlePrintAuditoriaPDF = () => { const printIframe = document.createElement('iframe'); printIframe.style.position = 'fixed'; printIframe.style.right = '0'; printIframe.style.bottom = '0'; printIframe.style.width = '0'; printIframe.style.height = '0'; printIframe.style.border = '0'; document.body.appendChild(printIframe); const companyTitle = settings?.companyName || window.SHIFT_CONFIG?.companyName || 'CLEANDENT'; const rangeText = startDate && endDate ? `${startDate} al ${endDate}` : (searchDate || 'Todo el Historial'); const rowsHtml = filteredLogs.map((log, idx) => { const audit = auditLogRecord(log); const dateFull = window.formatNICTimeFull ? window.formatNICTimeFull(log.timestamp) : log.date; const clientLoc = log.client ? ` • ${log.client}` : ''; let actionName = log.type.toUpperCase(); if (log.type === 'entrada') actionName = 'ENTRADA TRABAJO'; else if (log.type === 'salida') actionName = 'CIERRE DE DÍA (SALIDA)'; else if (log.type.includes('almuerzo')) actionName = log.type.includes('fin') ? 'FIN ALMUERZO' : 'INICIO ALMUERZO'; else if (log.isAbsenceRecord) actionName = log.type.toUpperCase(); const obsText = log.isAbsenceRecord ? `${log.justification} ${log.absenceType === 'injustificada' ? '(Deduce Séptimo Día)' : ''}` : audit.text; return `
Período Evaluado: ${rangeText} • Filtro: ${selectedActionType} • Total de Registros: ${filteredLogs.length} • Generado el ${todayStr}
| # | Colaborador / Sede | Actividad Registrada | Fecha y Hora | Observación de Auditoría |
|---|---|---|---|---|
| No se encontraron registros en el período seleccionado. | ||||
Quedará asentado en auditoría quién realizó el ajuste
Busca, filtra, exporta y corrige marcajes del personal
| Colaborador / Usuario | Actividad Registrada | Fecha y Hora | Observación de Auditoría | Pruebas Físicas |
|---|---|---|---|---|
| No se encontraron registros en el criterio seleccionado. | ||||
| {l.userName} | {l.type} | {window.formatDateToNIC(l.date)} | {l.justification} {l.absenceType === 'injustificada' ? '(Deduce Séptimo Día en Nómina)' : ''} | Ausencia |
| {l.userName || 'Usuario'} {l.client && ({l.client})} | {displayType} | {window.formatNICTimeFull ? window.formatNICTimeFull(l.timestamp) : new Date(l.timestamp).toLocaleString()} | {isOffDay && ( Marca en Día Libre )} {/* REGLA: SÓLO ES EDITABLE SI TIENE ANOMALÍA O YA FUE JUSTIFICADO */} {audit.isAnomaly ? ( ) : ( {audit.text} )} |
{l.location && l.location.lat ? (
) : N/A}
{l.photo ? (
X }
|
{justifyingLog.userName} • {justifyingLog.date}
Evaluación de estabilidad de datos real en tiempo real • Período: {simulationReportModal.summary?.period}
El motor de auditoría detectó correctamente marcaciones de entrada sin su correspondiente salida en el Día 6: