const { useState, useEffect, useRef, useMemo } = React; const UiIcon = ({ name, className = "w-6 h-6" }) => { const icons = { 'camera': , 'moon': , 'sun': , 'coffee': , 'clock': , 'briefcase': , 'users': , 'bell': , 'check-circle': , 'log-out': , 'settings': , 'shield': , 'x': }; return icons[name] || null; }; window.UserDashboard = function({ profile, logs, reminders = [], vacations = [], settings, actions = [], onLogout, setView, allRoles = [], hasAdminAccess }) { const [status, setStatus] = useState('fuera'); const [activeSubAction, setActiveSubAction] = useState(null); const [subActionStartTime, setSubActionStartTime] = useState(null); const [elapsedMinutes, setElapsedMinutes] = useState(0); const [showCam, setShowCam] = useState(false); const [purpose, setPurpose] = useState(''); const [clientName, setClientName] = useState(''); const [loadingMark, setLoadingMark] = useState(false); const [localLock, setLocalLock] = useState(false); const [gpsPermissionGranted, setGpsPermissionGranted] = useState(false); const liveCoordsRef = useRef(null); const [showOffDayModal, setShowOffDayModal] = useState(false); const [pendingActionType, setPendingActionType] = useState(''); const [tempJustification, setTempJustification] = useState(''); const [activeJustification, setActiveJustification] = useState(''); const [showClientModal, setShowClientModal] = useState(false); const [showNotifications, setShowNotifications] = useState(false); const [hasAutoOpenedNotices, setHasAutoOpenedNotices] = useState(false); const [showVacationModal, setShowVacationModal] = useState(false); const [vacReqType, setVacReqType] = useState('full'); const [vacReqStart, setVacReqStart] = useState(''); const [vacReqEnd, setVacReqEnd] = useState(''); const [toastMsg, setToastMsg] = useState(''); const videoRef = useRef(null); const canvasRef = useRef(null); const todayStr = window.getNICDate(); const showToast = (msg) => { setToastMsg(msg); setTimeout(() => setToastMsg(''), 3000); }; // Evaluación precisa de permisos de administración / supervisión const canAccessAdminPanel = useMemo(() => { if (hasAdminAccess !== undefined) return Boolean(hasAdminAccess); if (typeof window.checkHasAdminAccess === 'function') { return window.checkHasAdminAccess(profile); } const r = (profile?.role || profile?.role_name || '').toLowerCase().trim(); const acc = (profile?.accessLevel || profile?.access_level || '').toLowerCase().trim(); return r === 'admin' || r === 'propietario' || r === 'dueño' || acc === 'full' || acc === 'readonly' || acc === 'read_only' || Boolean(window.SHIFT_CONFIG?.isMaster); }, [hasAdminAccess, profile]); const isSupervisorOnly = useMemo(() => { const acc = (profile?.accessLevel || profile?.access_level || '').toLowerCase().trim(); return acc === 'readonly' || acc === 'read_only'; }, [profile]); const todayLogs = useMemo(() => { return (logs || []).filter(l => l.date === todayStr).sort((a,b) => b.timestamp - a.timestamp); }, [logs, todayStr]); const lunchLimitMinutes = Number(settings?.lunchLimitMinutes || settings?.lunch_limit_minutes || 60); const graceMinutes = Number(settings?.gracePeriodMinutes || settings?.grace_period_minutes || 5); const getActionLabel = (rawId) => { if (!rawId) return ''; const cleanId = rawId.replace('_in', '').replace('_out', '').replace('vuelta_', ''); const found = actions.find(a => a.id === cleanId || a.id === rawId); if (found) return found.label; if (cleanId === 'almuerzo') return 'Almuerzo'; if (cleanId === 'break') return 'Break'; if (cleanId === 'cirugia') return 'Cirugía'; if (cleanId === 'reunion') return 'Reunión'; return cleanId.toUpperCase(); }; const isTodayAnOffDay = useMemo(() => { return window.isUserOffDay(profile.scheduleDays, todayStr); }, [profile.scheduleDays, todayStr]); const isShiftCompleted = useMemo(() => { const hasSalida = todayLogs.some(l => l.type === 'salida'); return Boolean(profile.manualLock) || ((hasSalida || localLock) && !profile.overrideCheckOut); }, [todayLogs, profile.overrideCheckOut, profile.manualLock, localLock]); useEffect(() => { if (todayLogs.length === 0) { setStatus('fuera'); setActiveSubAction(null); setSubActionStartTime(null); return; } const newestLog = todayLogs[0]; const lastType = newestLog.type; if (lastType === 'salida') { setStatus('salida'); setActiveSubAction(null); setSubActionStartTime(null); } else if (lastType.endsWith('_in') && lastType !== 'entrada') { const baseAction = lastType.replace('_in', ''); setStatus(lastType); setActiveSubAction(baseAction); setSubActionStartTime(newestLog.timestamp); } else { setStatus('activo'); setActiveSubAction(null); setSubActionStartTime(null); } }, [todayLogs]); // RASTREO SATELITAL CONTINUO EN VIVO (GPS WATCH) useEffect(() => { if (!('geolocation' in navigator)) return; const geoOptions = { enableHighAccuracy: true, maximumAge: 0, timeout: 10000 }; const watchId = navigator.geolocation.watchPosition( (pos) => { const currentCoords = { lat: pos.coords.latitude, lng: pos.coords.longitude, accuracy: pos.coords.accuracy, timestamp: pos.timestamp }; liveCoordsRef.current = currentCoords; setGpsPermissionGranted(true); if (Boolean(profile.gpsTrackingEnabled) && !isShiftCompleted && status !== 'fuera' && status !== 'salida') { window.apiCall('ping_gps_tracker', { userId: profile.id, lat: currentCoords.lat, lng: currentCoords.lng }); } }, (err) => { console.warn("GPS Satelital en espera:", err.message); }, geoOptions ); return () => { if (watchId) navigator.geolocation.clearWatch(watchId); }; }, [profile.gpsTrackingEnabled, isShiftCompleted, status]); const requestManualGpsPermission = () => { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (pos) => { setGpsPermissionGranted(true); showToast("GPS Satelital Activado."); if (Boolean(profile.gpsTrackingEnabled)) { window.apiCall('ping_gps_tracker', { userId: profile.id, lat: pos.coords.latitude, lng: pos.coords.longitude }); } }, (err) => { showToast("Permiso de GPS denegado."); }, { enableHighAccuracy: true, timeout: 5000 } ); } }; useEffect(() => { if (!subActionStartTime) { setElapsedMinutes(0); return; } const updateTimer = () => { const diffMs = Date.now() - subActionStartTime; setElapsedMinutes(Math.floor(diffMs / 60000)); }; updateTimer(); const interval = setInterval(updateTimer, 15000); return () => clearInterval(interval); }, [subActionStartTime]); const activeVacationToday = useMemo(() => { return vacations.find(v => v.userId === profile.id && v.status === 'approved' && v.startDate <= todayStr && (v.endDate || v.startDate) >= todayStr && !(v.revokedDates||[]).includes(todayStr)); }, [vacations, profile.id, todayStr]); const myNotifications = useMemo(() => { const now = Date.now(); const activeAvisos = (reminders || []).filter(r => r.targetUsers?.includes(profile.id) && !(r.completedBy && r.completedBy[profile.id]) && (!r.expiresAt || r.expiresAt > now)).map(r => ({ ...r, notifType: 'aviso' })); const vacUpdates = (vacations || []).filter(v => v.userId === profile.id && (v.status === 'approved' || v.status === 'denied') && !v.seenByUser).map(v => ({ id: v.id, title: `Vacaciones ${v.status === 'approved' ? 'Aprobadas' : 'Denegadas'}`, message: v.type === 'paid' ? `Se te ha pagado el equivalente a ${v.paidDays} día(s).` : `Tu solicitud del ${v.startDate} fue ${v.status === 'approved' ? 'aprobada' : 'denegada'}.`, notifType: 'vacation', status: v.status })); return [...activeAvisos, ...vacUpdates]; }, [reminders, vacations, profile.id]); useEffect(() => { if (!hasAutoOpenedNotices && myNotifications.length > 0) { setShowNotifications(true); setHasAutoOpenedNotices(true); } }, [myNotifications, hasAutoOpenedNotices]); const markNotificationSeen = async (notif) => { try { await window.apiCall('mark_notification_seen', { id: notif.id, notifType: notif.notifType, userId: profile.id }); } catch (e) { showToast("Error al confirmar."); } }; const userRoleConfig = useMemo(() => { return allRoles.find(r => (r.name || '').toLowerCase() === (profile.role || '').toLowerCase()); }, [allRoles, profile.role]); const permissions = useMemo(() => { if (userRoleConfig && userRoleConfig.permissions && userRoleConfig.permissions.length > 0) { return userRoleConfig.permissions; } return actions.map(a => a.id); }, [userRoleConfig, actions]); const evaluateEntryTardiness = () => { if (isTodayAnOffDay || !profile.scheduleStart) return null; const now = new Date(); const currentMins = now.getHours() * 60 + now.getMinutes(); const [schedH, schedM] = profile.scheduleStart.split(':').map(Number); const schedMins = schedH * 60 + schedM; const diffMins = currentMins - schedMins; if (diffMins > graceMinutes) { return `ALERTA: Llegada Tardía de ${diffMins} min (Horario oficial: ${profile.scheduleStart}, Tolerancia: ${graceMinutes} min)`; } return null; }; const getFreshGPSLocation = async () => { return new Promise((resolve) => { if (!('geolocation' in navigator)) return resolve(null); if (liveCoordsRef.current) { return resolve({ lat: liveCoordsRef.current.lat, lng: liveCoordsRef.current.lng }); } navigator.geolocation.getCurrentPosition( (pos) => resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude }), () => resolve(null), { enableHighAccuracy: true, maximumAge: 0, timeout: 2500 } ); }); }; const executeActionWithCheck = async (p, explicitJustification = '') => { const baseKey = p.replace('_in', '').replace('_out', '').replace('vuelta_', ''); let requiresCam = false; if (userRoleConfig && userRoleConfig.cameraActions) { requiresCam = userRoleConfig.cameraActions.includes(baseKey) || userRoleConfig.cameraActions.includes(p); } else { requiresCam = p === 'entrada' || p === 'salida'; } const justToSave = explicitJustification || activeJustification || null; if (requiresCam) { proceedWithCam(p); } else { setLoadingMark(true); const freshLoc = await getFreshGPSLocation(); saveMark(p, null, freshLoc, justToSave); } }; const startAction = async (p) => { if (isShiftCompleted || activeVacationToday) return; if (isTodayAnOffDay && todayLogs.length === 0 && !activeJustification) { setPendingActionType(p); const reasons = settings?.exceptionReasons || ['Horas extras autorizadas', 'Solicitud de gerencia', 'Cobertura de turno especial', 'Emergencia operativa']; setTempJustification(reasons[0] || 'Horas extras autorizadas'); setShowOffDayModal(true); return; } if (p.includes('visita') && p.endsWith('_in') && !clientName) { setPurpose(p); setShowClientModal(true); return; } executeActionWithCheck(p); }; const handleConfirmOffDay = () => { const chosen = tempJustification || 'Horas extras autorizadas'; setActiveJustification(chosen); setShowOffDayModal(false); const actionToExec = pendingActionType || 'entrada'; setPendingActionType(''); executeActionWithCheck(actionToExec, chosen); }; const handleCancelOffDay = () => { setShowOffDayModal(false); setPendingActionType(''); setTempJustification(''); setActiveJustification(''); }; const proceedWithCam = async (p) => { setPurpose(p); setShowClientModal(false); setShowCam(true); const useRear = p.includes('visita'); const constraints = { video: { facingMode: useRear ? 'environment' : 'user', width: { ideal: 1280 }, height: { ideal: 720 } } }; try { const s = await navigator.mediaDevices.getUserMedia(constraints); if (videoRef.current) { videoRef.current.srcObject = s; if (!useRear) videoRef.current.classList.add('mirror'); else videoRef.current.classList.remove('mirror'); } } catch (err) { showToast("Cámara requerida."); setShowCam(false); } }; const capture = async () => { const canvas = canvasRef.current; const video = videoRef.current; canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); if (!purpose.includes('visita')) { ctx.translate(canvas.width, 0); ctx.scale(-1, 1); } ctx.drawImage(video, 0, 0); const photo = canvas.toDataURL('image/jpeg', 0.6); const freshLoc = await getFreshGPSLocation(); saveMark(purpose, photo, freshLoc, activeJustification); }; const saveMark = async (type, photo, loc, explicitJustification = null) => { setLoadingMark(true); let customJustification = explicitJustification || activeJustification || null; if (type === 'entrada' && !customJustification) { const tardyAlert = evaluateEntryTardiness(); if (tardyAlert) customJustification = tardyAlert; } if (type === 'almuerzo_out' && subActionStartTime) { const durationMins = Math.floor((Date.now() - subActionStartTime) / 60000); if (durationMins > lunchLimitMinutes) { const overMins = durationMins - lunchLimitMinutes; customJustification = `ALERTA: Almuerzo excedido por ${overMins} min (Duró ${durationMins} min / Límite: ${lunchLimitMinutes} min)`; } } try { await window.apiCall('save_log', { userId: profile.id, userName: profile.name, type, timestamp: Date.now(), location: loc, photo, client: clientName || null, date: todayStr, role: profile.role, justification: customJustification, update_user_override: type === 'salida' ? true : false }); if (type === 'salida') setLocalLock(true); if (videoRef.current?.srcObject) videoRef.current.srcObject.getTracks().forEach(t => t.stop()); setShowCam(false); setClientName(''); setActiveJustification(''); setTempJustification(''); if (customJustification && customJustification.startsWith('ALERTA:')) { showToast(`⚠️ ${customJustification}`); } else { showToast("Marca registrada exitosamente."); } } catch (e) { showToast("Error al guardar marca."); } finally { setLoadingMark(false); } }; const submitVacation = async () => { if (!vacReqStart || (vacReqType === 'range' && !vacReqEnd)) return showToast("Revisa las fechas."); const end = vacReqType === 'range' ? vacReqEnd : vacReqStart; if (vacReqType === 'range' && end < vacReqStart) return showToast("Fecha de fin inválida."); const daysReq = vacReqType === 'half' ? 0.5 : window.getDaysDiff(vacReqStart, end); const currentBal = window.calculateVacationBalance(profile, settings?.vacationRate||2.5, vacations); if (daysReq > currentBal) return showToast(`Saldo insuficiente. Pides ${daysReq} y tienes ${currentBal}`); try { await window.apiCall('add_vacation', { userId: profile.id, userName: profile.name, type: vacReqType, startDate: vacReqStart, endDate: end, status: 'pending', requestedAt: Date.now(), seenByUser: false }); setShowVacationModal(false); setVacReqStart(''); setVacReqEnd(''); setVacReqType('full'); showToast("Solicitud enviada a revisión."); } catch(e) { showToast("Error al solicitar."); } }; const isInsideActivity = activeSubAction !== null; const isOverLunchTime = activeSubAction === 'almuerzo' && elapsedMinutes > lunchLimitMinutes; const activeLabelReadable = isInsideActivity ? getActionLabel(activeSubAction) : ''; return (
Debes indicar la autorización para registrar esta jornada extraordinaria:
No tienes avisos pendientes.
) : ( myNotifications.map(n => ({n.message}
Saldo Disponible
{window.calculateVacationBalance(profile, settings?.vacationRate||2.5, vacations)} Días
Has registrado tu salida del día.
No tienes turno programado para hoy ({window.getDayOfWeekCode(todayStr).toUpperCase()})
{elapsedMinutes} min transcurridos
{activeSubAction === 'almuerzo' ? ({isOverLunchTime ? `⚠️ LÍMITE EXCEDIDO (+${elapsedMinutes - lunchLimitMinutes} MIN)` : `Límite permitido: ${lunchLimitMinutes} min`}
) : null}{window.formatNICTimeFull ? window.formatNICTimeFull(Date.now()).split(',')[0] : new Date().toLocaleDateString()}
Sin registros aún
: ({displayType} {l.client ? - {l.client} : null}
{window.formatNICTimeFull ? window.formatNICTimeFull(l.timestamp).split(', ')[1] : new Date(l.timestamp).toLocaleTimeString()}
{l.justification ? ({l.justification}
) : null}