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 (
{isVac ? `Tu acceso se habilitará a tu regreso el día hábil posterior al ${effectiveVacation?.effectiveEndDate || activeVacationToday.startDate}` : 'Acceso Inhabilitado temporalmente'}
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 />Todo limpio.
: myNotifications.map(n => ({n.message}
Saldo Disponible
{window.calculateVacationBalance(profile, settings?.vacationRate||2.5, vacations)} Días
{profile.role}
¡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
)}{window.formatNICTimeFull ? window.formatNICTimeFull(Date.now()).split(',')[0] : new Date().toLocaleDateString()}
Sin registros aún
: ({displayType} {l.client && - {l.client}}
{window.formatNICTimeFull ? window.formatNICTimeFull(l.timestamp).split(', ')[1] : new Date(l.timestamp).toLocaleTimeString()}