const { useState, useEffect, useRef, useMemo } = React; const UiIcon = ({ name, className = "w-6 h-6" }) => { const icons = { 'camera': , 'moon': , 'sun': , 'coffee': , 'clock': , 'briefcase': , 'users': , 'graduation-cap': , 'file-text': , 'bell': , 'check-circle': , 'log-out': , 'settings': , 'x': }; return icons[name] || null; }; window.UserDashboard = function({ profile, logs, reminders = [], vacations = [], settings, actions = [], onLogout, setView, allRoles }) { const [status, setStatus] = useState('fuera'); const [showCam, setShowCam] = useState(false); const [purpose, setPurpose] = useState(''); const [clientName, setClientName] = useState(''); const [loadingMark, setLoadingMark] = useState(false); const [localLock, setLocalLock] = useState(false); const [showClientModal, setShowClientModal] = useState(false); const [showNotifications, setShowNotifications] = 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); }; const todayLogs = useMemo(() => logs.filter(l => l.date === todayStr).sort((a,b) => a.timestamp - b.timestamp), [logs, todayStr]); 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 effectiveVacation = useMemo(() => { if (!activeVacationToday) return null; let lastDay = todayStr; let dt = new Date(todayStr); let endDt = new Date(activeVacationToday.endDate || activeVacationToday.startDate); while(dt <= endDt) { const y = dt.getFullYear(); const m = String(dt.getMonth() + 1).padStart(2, '0'); const d = String(dt.getDate()).padStart(2, '0'); const dStr = `${y}-${m}-${d}`; if ((activeVacationToday.revokedDates || []).includes(dStr)) { break; } lastDay = dStr; dt.setDate(dt.getDate() + 1); } return { ...activeVacationToday, effectiveEndDate: lastDay }; }, [activeVacationToday, todayStr]); const startsVacationTomorrow = useMemo(() => { let tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); const y = tomorrow.getFullYear(); const m = String(tomorrow.getMonth() + 1).padStart(2, '0'); const d = String(tomorrow.getDate()).padStart(2, '0'); const tomorrowStr = `${y}-${m}-${d}`; return vacations.find(v => v.userId === profile.id && v.status === 'approved' && v.startDate <= tomorrowStr && (v.endDate || v.startDate) >= tomorrowStr && !(v.revokedDates||[]).includes(tomorrowStr)); }, [vacations, profile.id]); useEffect(() => { if (todayLogs.length > 0) setStatus(todayLogs[todayLogs.length - 1].type); else setStatus('fuera'); }, [todayLogs]); 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]); 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(() => allRoles.find(r => (r.name || r) === profile.role), [allRoles, profile.role]); const permissions = userRoleConfig && userRoleConfig.permissions ? userRoleConfig.permissions : []; const isShiftCompleted = useMemo(() => { const hasSalida = todayLogs.some(l => l.type === 'salida'); return profile.manualLock || ((hasSalida || localLock) && !profile.overrideCheckOut); }, [todayLogs, profile.overrideCheckOut, profile.manualLock, localLock]); const executeActionWithCheck = (p) => { const baseP = p.startsWith('vuelta_') ? p.replace('vuelta_', '') : p.replace('_out', '_in'); let requiresCam = true; if (userRoleConfig && userRoleConfig.cameraActions) { requiresCam = userRoleConfig.cameraActions.includes(baseP) || userRoleConfig.cameraActions.includes(p); } else { requiresCam = !['almuerzo', 'vuelta_almuerzo', 'break', 'capacitacion', 'reunion', 'gestiones'].includes(p); } if (requiresCam) { proceedWithCam(p); } else { setLoadingMark(true); if (['entrada', 'salida', 'visita_in', 'visita_out'].includes(p)) { navigator.geolocation.getCurrentPosition( (pos) => saveMark(p, null, { lat: pos.coords.latitude, lng: pos.coords.longitude }), () => { showToast("GPS obligatorio."); setLoadingMark(false); } ); } else { saveMark(p, null, null); } } }; const startAction = async (p) => { if (isShiftCompleted || activeVacationToday) return; if (p.includes('visita') && !clientName) { setPurpose(p); setShowClientModal(true); return; } executeActionWithCheck(p); }; 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 = () => { 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); navigator.geolocation.getCurrentPosition( (pos) => saveMark(purpose, photo, { lat: pos.coords.latitude, lng: pos.coords.longitude }), () => { showToast("GPS obligatorio."); setShowCam(false); } ); }; const saveMark = async (type, photo, loc) => { setLoadingMark(true); try { await window.apiCall('save_log', { userId: profile.id, userName: profile.name, type, timestamp: Date.now(), location: loc, photo, client: clientName, date: todayStr, role: profile.role, 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(''); showToast("Marca registrada exitosamente."); } catch (e) { showToast("Error al guardar."); } 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."); } }; if (profile.status === 'subsidio' || profile.status === 'feriado' || activeVacationToday) { const isVac = !!activeVacationToday; return (

Estado: {isVac ? 'De Vacaciones' : profile.status}

{isVac ? `Tu acceso se habilitará a tu regreso el día hábil posterior al ${effectiveVacation?.effectiveEndDate || activeVacationToday.startDate}` : 'Acceso Inhabilitado temporalmente'}

); } return (
{toastMsg &&
{toastMsg}
} {showClientModal && (
setShowClientModal(false)}>
e.stopPropagation()}>

Visita Cliente

Ingresa la empresa que vas a visitar.

setClientName(e.target.value)} className="w-full p-4 bg-slate-50 rounded-2xl font-bold outline-none mb-4 focus:ring-2 focus:ring-indigo-100" autoFocus />
)} {showNotifications && (
setShowNotifications(false)}>
e.stopPropagation()}>

Avisos y Alertas

{myNotifications.length === 0 ?

Todo limpio.

: 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} />
)}
)}
{profile.name[0]}

{profile.name}

{profile.role}

{(profile.role === 'admin' || profile.role === 'propietario') && ( )}
{isShiftCompleted ? (
Jornada Finalizada {startsVacationTomorrow ? (

¡Descanso merecido! Tu acceso se habilitará el día hábil posterior al {startsVacationTomorrow.endDate || startsVacationTomorrow.startDate}.

) : (

Se habilitará mañana a las 12:00 AM

)}
) : (status === 'fuera' || status === 'salida') ? ( ) : (
ACTIVO

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

{actions.map(a => { if (!permissions.includes(a.id)) return null; let btnAction = a.id; let btnLabel = a.label; let isActiveToggle = false; if (a.id === 'almuerzo') { btnAction = status === 'almuerzo' ? 'vuelta_almuerzo' : 'almuerzo'; btnLabel = status === 'almuerzo' ? 'Regresar' : a.label; isActiveToggle = status === 'almuerzo'; } else if (a.id === 'visita_in') { btnAction = status === 'visita_in' ? 'visita_out' : 'visita_in'; btnLabel = status === 'visita_in' ? 'Saliendo' : a.label; isActiveToggle = status === 'visita_in'; } const iconMap = { 'almuerzo': 'coffee', 'break': 'clock', 'visita_in': 'briefcase', 'reunion': 'users', 'capacitacion': 'graduation-cap', 'gestiones': 'file-text' }; const iconName = iconMap[a.id] || 'check-circle'; return ( ); })}
)}

Bitácora de Hoy

{todayLogs.length === 0 ?

Sin registros aún

: (
{todayLogs.map(l => { const actionObj = actions.find(a => a.id === l.type.replace('vuelta_', '').replace('_out', '_in')); let displayType = l.type.replace('_', ' '); if (actionObj) { if (l.type.startsWith('vuelta_')) displayType = `Regreso ${actionObj.label}`; else if (l.type.endsWith('_out')) displayType = `Saliendo ${actionObj.label}`; else displayType = actionObj.label; } return (

{displayType} {l.client && - {l.client}}

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

); })}
)}
{showCam && (
)}
); };