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 (
{toastMsg &&
{toastMsg}
} {showOffDayModal && (
e.stopPropagation()}>

Hoy es tu Día Libre

Debes indicar la autorización para registrar esta jornada extraordinaria:

)} {showNotifications && (
setShowNotifications(false)}>
e.stopPropagation()}>

Avisos y Alertas

{myNotifications.length === 0 ? (

No tienes avisos pendientes.

) : ( myNotifications.map(n => (

{n.title}

{n.message}

)) )}
)} {showVacationModal && (
setShowVacationModal(false)}>
e.stopPropagation()}>

Mis Vacaciones

Saldo Disponible

{window.calculateVacationBalance(profile, settings?.vacationRate||2.5, vacations)} Días

setVacReqStart(e.target.value)} className="w-full p-4 bg-slate-50 rounded-2xl font-bold outline-none" min={todayStr} />
{vacReqType === 'range' && (
setVacReqEnd(e.target.value)} className="w-full p-4 bg-slate-50 rounded-2xl font-bold outline-none" min={vacReqStart || todayStr} />
)}
)} {/* HEADER CON FOTO DE PERFIL, ACCESO A SUPERVISIÓN Y BOTONES */}
{profile.avatarUrl ? ( Avatar ) : (
{profile.name ? profile.name[0].toUpperCase() : 'U'}
)}

{profile.name}

{profile.role} {Boolean(profile.gpsTrackingEnabled) ? ( ) : null}
{canAccessAdminPanel && ( )}
{/* BOTÓN INFORMATIVO DE ACTIVACIÓN TÁCTIL EN IPHONE */} {Boolean(profile.gpsTrackingEnabled) && !gpsPermissionGranted ? ( ) : null} {/* ACCIONES RÁPIDAS (VACACIONES Y ACCESO AL PANEL DE ADMINISTRACIÓN/SUPERVISIÓN) */}
{canAccessAdminPanel ? ( ) : null}
{isShiftCompleted ? (

Jornada Finalizada

Has registrado tu salida del día.

¿Reingreso extraordinario? Solicita el desbloqueo a tu Administrador.
) : (status === 'fuera') ? ( isTodayAnOffDay ? (

Hoy es tu Día de Descanso

No tienes turno programado para hoy ({window.getDayOfWeekCode(todayStr).toUpperCase()})

) : ( ) ) : (
{isInsideActivity ? (
{activeLabelReadable.toUpperCase()} EN CURSO

{elapsedMinutes} min transcurridos

{activeSubAction === 'almuerzo' ? (

{isOverLunchTime ? `⚠️ LÍMITE EXCEDIDO (+${elapsedMinutes - lunchLimitMinutes} MIN)` : `Límite permitido: ${lunchLimitMinutes} min`}

) : null}
) : (
ACTIVO

{window.formatNICTimeFull ? window.formatNICTimeFull(Date.now()).split(',')[0] : new Date().toLocaleDateString()}

)}
{actions.map(a => { if (!permissions.includes(a.id)) return null; const isThisActionActive = activeSubAction === a.id; const isBlockedByOtherAction = isInsideActivity && !isThisActionActive; const actionTarget = isThisActionActive ? `${a.id}_out` : `${a.id}_in`; const displayLabel = isThisActionActive ? `Regresar de ${a.label}` : a.label; const iconMap = { 'almuerzo': 'coffee', 'break': 'clock', 'visita_in': 'briefcase', 'reunion': 'users' }; const iconName = iconMap[a.id] || 'check-circle'; return ( ); })}
)}
{/* BITÁCORA DESCENDENTE */}

Bitácora de Hoy (Recientes Primero)

{todayLogs.length === 0 ?

Sin registros aún

: (
{todayLogs.map(l => { const rawActionId = l.type.replace('_in', '').replace('_out', '').replace('vuelta_', ''); const labelStr = getActionLabel(rawActionId); let displayType = l.type.replace('_', ' '); if (l.type.endsWith('_in') && l.type !== 'entrada') displayType = `Inició ${labelStr}`; else if (l.type.endsWith('_out') || l.type.startsWith('vuelta_')) displayType = `Regresó de ${labelStr}`; else if (l.type === 'entrada') displayType = 'Entrada Trabajo'; else if (l.type === 'salida') displayType = 'Cierre de Día (Salida)'; const isAlert = l.justification && l.justification.startsWith('ALERTA:'); return (

{displayType} {l.client ? - {l.client} : null}

{window.formatNICTimeFull ? window.formatNICTimeFull(l.timestamp).split(', ')[1] : new Date(l.timestamp).toLocaleTimeString()}

{l.justification ? (

{l.justification}

) : null}
); })}
)}
{showCam && (
)}
); };