const { useState, useEffect, useRef, useMemo } = React; // ========================================== // COMPONENTE: VISTA DE LOGIN // ========================================== function LoginView({ setView, setProfile, hasUsers, allUsers }) { const [email, setEmail] = useState(''); const [pass, setPass] = useState(''); const [name, setName] = useState(''); const [error, setError] = useState(''); const isSetup = !hasUsers; const handleLogin = async (e) => { e.preventDefault(); if (isSetup) { const admin = { name, email: email.toLowerCase().trim(), password: pass, role: 'admin', status: 'activo', hiringDate: window.getNICDate(), vacationDaysUsed: 0, overrideCheckOut: false }; const ref = await db.collection('artifacts').doc(appId).collection('public').doc('data').collection('users').add(admin); const p = { id: ref.id, ...admin }; setProfile(p); localStorage.setItem('shift_session_v4', JSON.stringify(p)); setView('admin'); } else { const found = allUsers.find(u => u.email === email.toLowerCase().trim() && u.password === pass); if (found) { if (found.status === 'baja') return setError("Cuenta desactivada por administración."); setProfile(found); localStorage.setItem('shift_session_v4', JSON.stringify(found)); setView(found.role === 'admin' ? 'admin' : 'dashboard'); } else setError("Credenciales incorrectas."); } }; return (

{isSetup ? "Setup Maestro" : "Shift Marcas"}

Nicaragua (UTC-6)

{isSetup && setName(e.target.value)} className="w-full p-4 bg-slate-50 border-2 border-slate-100 rounded-2xl outline-none" required />} setEmail(e.target.value)} className="w-full p-4 bg-slate-50 border-2 border-slate-100 rounded-2xl outline-none" required /> setPass(e.target.value)} className="w-full p-4 bg-slate-50 border-2 border-slate-100 rounded-2xl outline-none" required /> {error &&
{error}
}
); } // ========================================== // COMPONENTE: DASHBOARD DEL CONSULTOR // ========================================== function UserDashboard({ profile, logs, reminders = [], 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 videoRef = useRef(null); const canvasRef = useRef(null); const todayStr = window.getNICDate(); const todayLogs = useMemo(() => logs.filter(l => l.date === todayStr).sort((a,b) => a.timestamp - b.timestamp), [logs, todayStr]); useEffect(() => { if (todayLogs.length > 0) setStatus(todayLogs[todayLogs.length - 1].type); else setStatus('fuera'); }, [todayLogs]); // Filtrar notificaciones activas para este usuario const activeReminders = useMemo(() => { const now = Date.now(); return reminders.filter(r => r.targetUsers.includes(profile.id) && !r.completedBy[profile.id] && (!r.expiresAt || r.expiresAt > now) ); }, [reminders, profile.id]); const markReminderCompleted = async (rId) => { try { const rRef = db.collection('artifacts').doc(appId).collection('public').doc('data').collection('reminders').doc(rId); const rDoc = await rRef.get(); const completedBy = rDoc.data().completedBy || {}; completedBy[profile.id] = Date.now(); await rRef.update({ completedBy }); } catch (e) { alert("Error al confirmar lectura."); } }; // Obtener los permisos del rol actual (Soporta estructura antigua de strings) const userRoleConfig = useMemo(() => allRoles.find(r => (r.name || r) === profile.role), [allRoles, profile.role]); const permissions = userRoleConfig && userRoleConfig.permissions ? userRoleConfig.permissions : []; // Bloqueo de jornada si ya marcó salida (con bypass administrativo) const isShiftCompleted = useMemo(() => { const hasSalida = todayLogs.some(l => l.type === 'salida'); return hasSalida && !profile.overrideCheckOut; }, [todayLogs, profile.overrideCheckOut]); const startAction = async (p) => { if (isShiftCompleted) return; // Acciones sin cámara (configurables) if (['almuerzo', 'vuelta_almuerzo', 'break', 'capacitacion', 'reunion', 'gestiones'].includes(p)) { return saveMark(p, null, null); } if (p === 'visita_in') { const name = prompt("Escriba el nombre del Cliente / Referencia:"); if (!name) return; setClientName(name); } setPurpose(p); 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) { alert("Error al activar cámara."); 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 }), () => { alert("GPS obligatorio para auditar ubicación."); setShowCam(false); }, { enableHighAccuracy: true } ); }; const saveMark = async (type, photo, loc) => { setLoadingMark(true); try { await db.collection('artifacts').doc(appId).collection('public').doc('data').collection('logs').add({ userId: profile.id, userName: profile.name, type, timestamp: Date.now(), location: loc, photo, client: clientName, date: todayStr, role: profile.role }); // Si es salida, reseteamos el desbloqueo administrativo para el día siguiente if (type === 'salida' && profile.overrideCheckOut) { await db.collection('artifacts').doc(appId).collection('public').doc('data').collection('users').doc(profile.id).update({ overrideCheckOut: false }); } if (videoRef.current?.srcObject) videoRef.current.srcObject.getTracks().forEach(t => t.stop()); setShowCam(false); setClientName(''); } catch (e) { alert("Error de red al guardar marca."); } finally { setLoadingMark(false); } }; if (profile.status === 'subsidio' || profile.status === 'feriado') return (

Estado: {profile.status}

No habilitado para marcas hoy

); return (
{/* ALERTAS/NOTIFICACIONES PENDIENTES */} {activeReminders.length > 0 && (
{activeReminders.map(r => (

{r.title}

{r.message}

))}
)}
{profile.name[0]}

{profile.name}

{profile.role}

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

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

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

¡Que tengas un excelente día!

{/* BOTONES DINÁMICOS BASADOS EN LOS PERMISOS DEL ROL SELECCIONADO POR EL ADMIN */} {permissions.includes('almuerzo') && ( )} {permissions.includes('break') && ( )} {permissions.includes('visita_in') && ( )} {permissions.includes('reunion') && ( )} {permissions.includes('capacitacion') && ( )} {permissions.includes('gestiones') && ( )}
)}

Mi Actividad de Hoy

{todayLogs.length === 0 ?

No hay marcas registradas todavía

: (
{todayLogs.map(l => (

{l.type.replace('_',' ')} {l.client && @ {l.client}}

{new Date(l.timestamp).toLocaleTimeString('es-NI', {timeZone: 'America/Managua', hour:'2-digit', minute:'2-digit', hour12: true})}

))}
)}
{showCam && (

Protocolo Biométrico

)}
); } // ========================================== // COMPONENTE: PANEL DE ADMINISTRACIÓN // ========================================== function AdminDashboard({ globalUsers, logs, roles, reminders = [], setView, onLogout }) { const [tab, setTab] = useState('logs'); const [searchName, setSearchName] = useState(''); const [searchDate, setSearchDate] = useState(window.getNICDate()); const [editingUser, setEditingUser] = useState(null); const [editingLog, setEditingLog] = useState(null); const [userToDelete, setUserToDelete] = useState(null); const [submitError, setSubmitError] = useState(''); // Estados para Roles const [newRoleName, setNewRoleName] = useState(''); const [selectedPermissions, setSelectedPermissions] = useState([]); const availableActions = [ { id: 'almuerzo', label: 'Almuerzo' }, { id: 'break', label: 'Break' }, { id: 'visita_in', label: 'Visita Cliente' }, { id: 'reunion', label: 'Reunión' }, { id: 'capacitacion', label: 'Capacitación' }, { id: 'gestiones', label: 'Gestiones' } ]; // Estados para Notificaciones const [notifTitle, setNotifTitle] = useState(''); const [notifBody, setNotifBody] = useState(''); const [notifExpiry, setNotifExpiry] = useState(''); const [targetUsers, setTargetUsers] = useState([]); const filteredLogs = useMemo(() => logs.filter(l => { const matchName = l.userName.toLowerCase().includes(searchName.toLowerCase()); const matchDate = searchDate ? l.date === searchDate : true; return matchName && matchDate; }).sort((a,b) => b.timestamp - a.timestamp), [logs, searchName, searchDate]); const handleUserSubmit = async (e) => { e.preventDefault(); setSubmitError(''); const f = new FormData(e.target); const email = f.get('email').toLowerCase().trim(); // VALIDACIÓN DE DUPLICIDAD if (!editingUser) { const existing = globalUsers.find(u => u.email === email); if (existing) { setSubmitError(`ADVERTENCIA: Correo ya asignado a ${existing.name} (${existing.status.toUpperCase()})`); return; } } const data = { name: f.get('name'), email, password: f.get('pass'), role: f.get('role'), status: f.get('status'), hiringDate: f.get('date'), vacationDaysUsed: parseInt(f.get('vused')) || 0, vacationStart: f.get('vs') || '', vacationEnd: f.get('ve') || '', overrideCheckOut: editingUser ? (editingUser.overrideCheckOut || false) : false }; try { if(editingUser) await db.collection('artifacts').doc(appId).collection('public').doc('data').collection('users').doc(editingUser.id).update(data); else await db.collection('artifacts').doc(appId).collection('public').doc('data').collection('users').add(data); setEditingUser(null); } catch (err) { setSubmitError("Error al guardar datos."); } }; const deleteUser = async () => { if (!userToDelete) return; try { await db.collection('artifacts').doc(appId).collection('public').doc('data').collection('users').doc(userToDelete.id).delete(); setUserToDelete(null); } catch (err) { alert("Error al eliminar."); } }; const unlockUser = async (uId) => { try { await db.collection('artifacts').doc(appId).collection('public').doc('data').collection('users').doc(uId).update({ overrideCheckOut: true }); alert("Usuario habilitado para re-entrar hoy."); } catch (err) { alert("Error al desbloquear."); } }; const updateLog = async (e) => { e.preventDefault(); const f = new FormData(e.target); const newTimeStr = f.get('ntime'); const newTimestamp = new Date(newTimeStr).getTime(); const newDate = newTimeStr.split('T')[0]; try { await db.collection('artifacts').doc(appId).collection('public').doc('data').collection('logs').doc(editingLog.id).update({ timestamp: newTimestamp, type: f.get('ntype'), client: f.get('nclient'), edited: true, date: newDate }); setEditingLog(null); } catch (err) { alert("Error al actualizar marca."); } }; const togglePermission = (id) => { setSelectedPermissions(prev => prev.includes(id) ? prev.filter(p => p !== id) : [...prev, id]); }; const addRole = async () => { if(!newRoleName) return; try { const newRoleObj = { name: newRoleName.toLowerCase().trim(), permissions: selectedPermissions }; // Filtrar tanto objetos como strings antiguos para evitar errores const updated = [...roles.filter(r => (r.name || r) !== newRoleObj.name), newRoleObj]; await db.collection('artifacts').doc(appId).collection('public').doc('data').collection('settings').doc('roles').set({ list: updated }); setNewRoleName(''); setSelectedPermissions([]); } catch (err) { alert("Error al guardar rol."); } }; const sendNotification = async () => { if (!notifTitle || targetUsers.length === 0) return alert("Completa el título y elige al menos un usuario."); try { await db.collection('artifacts').doc(appId).collection('public').doc('data').collection('reminders').add({ title: notifTitle, message: notifBody, expiresAt: notifExpiry ? new Date(notifExpiry).getTime() : null, targetUsers: targetUsers, completedBy: {}, createdAt: Date.now(), createdBy: 'Admin' }); setNotifTitle(''); setNotifBody(''); setNotifExpiry(''); setTargetUsers([]); alert("Notificación enviada correctamente."); } catch (e) { alert("Error al enviar."); } }; return (
{tab === 'logs' && (
setSearchName(e.target.value)} placeholder="Nombre..." className="w-full p-4 bg-slate-50 border rounded-xl outline-none font-bold" />
setSearchDate(e.target.value)} className="w-full p-4 bg-slate-50 border rounded-xl outline-none font-bold" />
{filteredLogs.map(l => ( ))}
ColaboradorMarcaFecha/Hora (12h)AuditoríaAcción
{l.type.replace('_',' ')} {l.client && `(${l.client})`} {new Date(l.timestamp).toLocaleString('es-NI', {timeZone: 'America/Managua', day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: true})} {l.edited &&
Editado por Admin
}
{l.photo && } {l.location && }
)} {tab === 'users' && (

{editingUser ? "Editar Perfil" : "Nuevo Registro"}

{submitError &&
{submitError}
}
{editingUser && }
{globalUsers.map(u => (
{u.name[0]}

{u.name}

{u.role} • {u.status} {u.overrideCheckOut && (DESBLOQUEADO)}

))}
)} {tab === 'roles' && (

Configurar Rol y Permisos

setNewRoleName(e.target.value)} placeholder="Nombre del Rol (ej: Ventas)" className="w-full p-4 bg-slate-50 border-2 border-slate-100 rounded-2xl outline-none font-bold uppercase focus:border-indigo-400" />

Habilitar botones para este rol:

{availableActions.map(action => ( ))}
{roles.map(r => { const rName = r.name || r; const rPerms = r.permissions || []; return (

{rName}

{rPerms.map(p => {p})} {rPerms.length === 0 && Sin permisos adicionales}
) })}
)} {/* TAB: NOTIFICACIONES */} {tab === 'notif' && (

Nuevo Aviso

setNotifTitle(e.target.value)} placeholder="Título del aviso" className="w-full p-4 bg-slate-50 border-2 rounded-2xl font-bold outline-none" />