const { useState, useEffect, useRef, useMemo } = React; const AdminIcon = ({ name, className = "w-6 h-6" }) => { const icons = { 'map-pin': , 'x': , 'chevron-left': , 'chevron-right': , 'edit': , 'plus': , 'switch': }; return icons[name] || null; }; function RouteMapModal({ logs, onClose }) { const mapRef = useRef(null); const mapInstance = useRef(null); useEffect(() => { const loadLeaflet = async () => { if (!window.L) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; document.head.appendChild(link); const script = document.createElement('script'); script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'; document.head.appendChild(script); await new Promise(r => script.onload = r); } initMap(); }; loadLeaflet(); return () => { if (mapInstance.current) { mapInstance.current.remove(); mapInstance.current = null; } }; }, []); const initMap = () => { if (!mapRef.current || !window.L) return; const validLogs = logs.filter(l => l.location && l.location.lat).sort((a,b) => a.timestamp - b.timestamp); if (validLogs.length === 0) { mapRef.current.innerHTML = '
El usuario no registró GPS en este(os) marcaje(s).
'; return; } mapInstance.current = window.L.map(mapRef.current).setView([validLogs[0].location.lat, validLogs[0].location.lng], 15); window.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap' }).addTo(mapInstance.current); const latlngs = []; validLogs.forEach((l) => { const pt = [l.location.lat, l.location.lng]; latlngs.push(pt); const time = window.formatNICTimeFull ? window.formatNICTimeFull(l.timestamp).split(', ')[1] : new Date(l.timestamp).toLocaleTimeString(); const color = l.type.includes('entrada') ? '#10b981' : l.type.includes('salida') ? '#ef4444' : '#6366f1'; const iconHtml = `
`; const customIcon = window.L.divIcon({ html: iconHtml, className: '', iconSize: [16,16], iconAnchor: [8,8] }); window.L.marker(pt, {icon: customIcon}).addTo(mapInstance.current) .bindPopup(`
${l.type.replace('_',' ')}
${time}${l.client?`

Visita a:
${l.client}`:''}
`); }); if (latlngs.length > 1) { const polyline = window.L.polyline(latlngs, {color: '#4f46e5', weight: 4, dashArray: '5, 10'}).addTo(mapInstance.current); mapInstance.current.fitBounds(polyline.getBounds(), { padding: [50, 50] }); } }; return (
e.stopPropagation()}>

Auditoría Geográfica

{logs.length > 1 ? 'Ruta trazada durante el día' : 'Punto de marcaje exacto'}

); } window.AdminDashboard = function({ globalUsers, logs, roles, reminders = [], vacations = [], settings, actions = [], absences = [], masterClients = [], activeClientId, setActiveClientId, refreshData, setView, onLogout }) { const isMaster = window.SHIFT_CONFIG && window.SHIFT_CONFIG.isMaster; const [tab, setTab] = useState(isMaster ? 'master' : 'logs'); const [toastMsg, setToastMsg] = useState(''); // SaaS Maestro State const [showClientModal, setShowClientModal] = useState(false); const [editingClient, setEditingClient] = useState(null); // UI Auditoría const [searchName, setSearchName] = useState(''); const [searchDate, setSearchDate] = useState(window.getNICDate()); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [mapLogs, setMapLogs] = useState(null); // UI Gestión General const [editingUser, setEditingUser] = useState(null); const [userToDelete, setUserToDelete] = useState(null); const [previewImage, setPreviewImage] = useState(null); const [confirmModal, setConfirmModal] = useState(null); // Acciones y Botones const [editingActionId, setEditingActionId] = useState(null); const [newActionLabel, setNewActionLabel] = useState(''); // Roles & Avisos const [editingRoleName, setEditingRoleName] = useState(null); const [newRoleName, setNewRoleName] = useState(''); const [selectedPermissions, setSelectedPermissions] = useState([]); const [selectedCameraActions, setSelectedCameraActions] = useState([]); const [editingReminderId, setEditingReminderId] = useState(null); const [notifTitle, setNotifTitle] = useState(''); const [notifBody, setNotifBody] = useState(''); const [targetUsers, setTargetUsers] = useState([]); // Vacaciones const [vacRate, setVacRate] = useState(settings?.vacationRate || 2.5); const [selectedUserVacation, setSelectedUserVacation] = useState(null); const [managingVacation, setManagingVacation] = useState(null); const [tempRevokedDates, setTempRevokedDates] = useState([]); const [directVacType, setDirectVacType] = useState('full'); const [directVacStart, setDirectVacStart] = useState(''); const [directVacEnd, setDirectVacEnd] = useState(''); const [directPaidDays, setDirectPaidDays] = useState(1); const [vacPage, setVacPage] = useState(0); // Ausencias const [absenceUser, setAbsenceUser] = useState(''); const [absenceDate, setAbsenceDate] = useState(window.getNICDate()); const [absenceType, setAbsenceType] = useState('injustificada'); const [absenceNotes, setAbsenceNotes] = useState(''); const showToast = (msg) => { setToastMsg(msg); setTimeout(() => setToastMsg(''), 3000); }; const confirmAction = (message, action) => setConfirmModal({ message, action }); const currentCompanyObj = useMemo(() => { if (!masterClients || masterClients.length === 0) return null; return masterClients.find(c => c.id === activeClientId); }, [masterClients, activeClientId]); const handleSaveClient = async (e) => { e.preventDefault(); const f = new FormData(e.target); const payload = { id: editingClient ? editingClient.id : null, company_name: f.get('company_name'), shift_subdomain: f.get('shift_subdomain'), contact_name: f.get('contact_name'), contact_email: f.get('contact_email'), contact_phone: f.get('contact_phone'), module_shift: f.get('module_shift') ? 1 : 0, admin_password: f.get('admin_password') || '' }; const res = await window.apiCall('save_master_client', payload); if (res.success) { setShowClientModal(false); setEditingClient(null); if (refreshData) refreshData(); showToast("Empresa guardada exitosamente."); } else showToast(res.error || "Error al guardar empresa."); }; const toggleModule = async (c) => { await window.apiCall('toggle_client_module', { id: c.id, module_shift: c.module_shift ? 0 : 1 }); if (refreshData) refreshData(); showToast("Estado de módulo actualizado."); }; const filteredLogs = useMemo(() => { return logs.filter(l => { const matchName = (l.userName || '').toLowerCase().includes(searchName.toLowerCase()); let matchDate = true; if (searchDate) { matchDate = l.date === searchDate; } else if (startDate && endDate) { matchDate = l.date >= startDate && l.date <= endDate; } return matchName && matchDate; }).sort((a,b) => b.timestamp - a.timestamp); }, [logs, searchName, searchDate, startDate, endDate]); const handleUserSubmit = async (e) => { e.preventDefault(); const f = new FormData(e.target); const email = f.get('email').toLowerCase().trim(); const aliasCode = f.get('alias').trim(); if (!editingUser) { const existingEmail = globalUsers.find(u => u.email === email); if (existingEmail) return showToast(`ADVERTENCIA: Correo en uso por "${existingEmail.name}"`); } const data = { id: editingUser ? editingUser.id : null, name: f.get('name'), alias: aliasCode, email, password: f.get('pass'), role: f.get('role'), status: f.get('status'), hiringDate: f.get('date'), vacationDaysUsed: parseFloat(f.get('vused')) || 0, overrideCheckOut: editingUser ? (editingUser.overrideCheckOut || false) : false, manualLock: editingUser ? (editingUser.manualLock || false) : false }; try { await window.apiCall('save_user', data); setEditingUser(null); e.target.reset(); showToast("Datos guardados."); if (refreshData) refreshData(); } catch (err) { showToast("Error de red al guardar usuario."); } }; const deleteUser = async (id) => { await window.apiCall('delete_user', { id }); setUserToDelete(null); setConfirmModal(null); showToast("Usuario borrado."); if (refreshData) refreshData(); }; const isUserLockedToday = (user) => { const todayLogs = logs.filter(l => l.userId === user.id && l.date === window.getNICDate()); const hasSalida = todayLogs.some(l => l.type === 'salida'); return user.manualLock || (hasSalida && !user.overrideCheckOut); }; const toggleLock = async (u, lockStatus) => { await window.apiCall('toggle_user_lock', { id: u.id, manualLock: lockStatus, overrideCheckOut: !lockStatus }); showToast(lockStatus ? "Bloqueado." : "Desbloqueado."); if (refreshData) refreshData(); }; const saveCustomAction = async () => { if (!newActionLabel.trim()) return showToast("Escribe un nombre para el botón."); const newId = editingActionId || newActionLabel.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '') + '_' + Date.now().toString().slice(-4); await window.apiCall('save_action', { action_id: newId, label: newActionLabel }); setEditingActionId(null); setNewActionLabel(''); showToast("Botón guardado exitosamente."); if (refreshData) refreshData(); }; const deleteCustomAction = async (act) => { confirmAction(`¿Borrar el botón "${act.label}" de todo el sistema?`, async () => { await window.apiCall('delete_action', { action_id: act.id }); setConfirmModal(null); showToast("Botón eliminado."); if (refreshData) refreshData(); }); }; const updateVacationRate = async () => { await window.apiCall('update_settings', { vacation_rate: parseFloat(vacRate) || 2.5 }); showToast("Tasa mensual actualizada."); if (refreshData) refreshData(); }; const setVacationStatus = async (vId, status) => { await window.apiCall('update_vacation_status', { id: vId, status }); showToast(`Actualizado a: ${status}`); if (refreshData) refreshData(); }; const handleDirectAssignment = async (e) => { e.preventDefault(); if (!selectedUserVacation) return; let payload = { userId: selectedUserVacation.id, userName: selectedUserVacation.name, type: directVacType, status: 'approved', requestedAt: Date.now(), seenByUser: false, revokedDates: [] }; if (directVacType === 'paid') { if (!directPaidDays || directPaidDays <= 0) return showToast("Cantidad de días inválida."); payload.paidDays = Number(directPaidDays); } else { if (!directVacStart || (directVacType === 'range' && !directVacEnd)) return showToast("Faltan fechas."); const end = directVacType === 'range' ? directVacEnd : directVacStart; if (directVacType === 'range' && end < directVacStart) return showToast("Fecha de fin debe ser mayor."); payload.startDate = directVacStart; payload.endDate = end; } await window.apiCall('add_vacation', payload); setDirectVacStart(''); setDirectVacEnd(''); setDirectVacType('full'); setDirectPaidDays(1); showToast("Acción guardada exitosamente."); if (refreshData) refreshData(); }; const toggleRevokeDay = (dateStr) => { setTempRevokedDates(prev => prev.includes(dateStr) ? prev.filter(d => d !== dateStr) : [...prev, dateStr]); }; const confirmRevokedDates = async () => { if (!managingVacation) return; await window.apiCall('update_revoked_dates', { id: managingVacation.id, revokedDates: tempRevokedDates }); setManagingVacation(null); showToast("Días actualizados en el saldo del usuario."); if (refreshData) refreshData(); }; const saveAbsence = async (e) => { e.preventDefault(); if (!absenceUser || !absenceDate) return showToast("Falta usuario o fecha."); const userObj = globalUsers.find(u => u.id == absenceUser); await window.apiCall('save_absence', { userId: userObj.id, userName: userObj.name, date: absenceDate, type: absenceType, notes: absenceNotes, createdAt: Date.now() }); if (absenceType === 'descontada') { await window.apiCall('add_vacation', { userId: userObj.id, userName: userObj.name, type: 'full', startDate: absenceDate, endDate: absenceDate, status: 'approved', requestedAt: Date.now(), seenByUser: true, revokedDates: [], notes: 'Ausencia Convertida a Vacación' }); } setAbsenceUser(''); setAbsenceNotes(''); showToast("Ausencia Registrada."); if (refreshData) refreshData(); }; const saveRole = async () => { if (!newRoleName) return showToast("Escribe un nombre."); await window.apiCall('save_role', { name: newRoleName.toLowerCase().trim(), oldName: editingRoleName, permissions: selectedPermissions, cameraActions: selectedCameraActions }); setEditingRoleName(null); setNewRoleName(''); setSelectedPermissions([]); setSelectedCameraActions([]); showToast("Rol guardado."); if (refreshData) refreshData(); }; const saveNotification = async () => { if (!notifTitle || targetUsers.length === 0) return showToast("Faltan datos en el aviso."); await window.apiCall('save_reminder', { id: editingReminderId, title: notifTitle, message: notifBody, targetUsers: targetUsers, createdAt: Date.now() }); setEditingReminderId(null); setNotifTitle(''); setNotifBody(''); setTargetUsers([]); showToast("Aviso enviado."); if (refreshData) refreshData(); }; const approvedVacsHistory = useMemo(() => vacations.filter(v => v.status === 'approved').sort((a,b)=>new Date(b.startDate)-new Date(a.startDate)), [vacations]); const totalVacPages = Math.ceil(approvedVacsHistory.length / 5); const paginatedVacs = approvedVacsHistory.slice(vacPage * 5, (vacPage + 1) * 5); return (
{toastMsg &&
{toastMsg}
} {/* MODAL CREAR / EDITAR EMPRESA SAAS */} {showClientModal && (
setShowClientModal(false)}>
e.stopPropagation()}>

{editingClient ? "Editar Empresa SaaS" : "Nueva Empresa SaaS"}

{!editingClient && (
)}
)} {mapLogs && setMapLogs(null)} />} {previewImage && (
setPreviewImage(null)}>
e.stopPropagation()}>
)} {confirmModal && (
setConfirmModal(null)}>
e.stopPropagation()}>

Confirmación

{confirmModal.message}

)} {selectedUserVacation && (
setSelectedUserVacation(null)}>
e.stopPropagation()}>

{selectedUserVacation.name}

REPORTE VACACIONAL • SALDO: {window.calculateVacationBalance(selectedUserVacation, settings?.vacationRate||2.5, vacations)} DÍAS

ASIGNACIÓN DIRECTA ADMINISTRADOR

{directVacType === 'paid' ? ( setDirectPaidDays(e.target.value)} className="w-full p-4 bg-slate-50 rounded-2xl font-bold outline-none col-span-2 md:col-span-1" placeholder="Días a pagar..." required/> ) : ( setDirectVacStart(e.target.value)} className={`w-full p-4 bg-slate-50 rounded-2xl font-bold outline-none ${directVacType === 'range' ? 'col-span-1' : 'col-span-2 md:col-span-1'}`} required/> )} {directVacType === 'range' && setDirectVacEnd(e.target.value)} className="w-full p-4 bg-slate-50 rounded-2xl font-bold outline-none" required min={directVacStart}/>} {directVacType !== 'range' && }
{directVacType === 'range' && }

HISTORIAL Y SOLICITUDES DEL USUARIO

{vacations.filter(v=>v.userId === selectedUserVacation.id).length === 0 ?

Sin registros en el sistema.

: vacations.filter(v=>v.userId === selectedUserVacation.id).sort((a,b)=>new Date(b.startDate)-new Date(a.startDate)).map(v => (

{v.type === 'half' ? 'MEDIO DÍA (0.5)' : v.type === 'paid' ? 'PAGO VACACIONES' : 'RANGO/DÍA COMPLETO'}

{v.type === 'paid' ?

DÍAS LIQUIDADOS: {v.paidDays}

:

FECHAS: {window.formatDateToNIC ? window.formatDateToNIC(v.startDate) : v.startDate} {v.type==='range'?` AL ${window.formatDateToNIC ? window.formatDateToNIC(v.endDate) : v.endDate}`:''}

}
{v.status === 'approved' ? 'APROBADO' : v.status === 'pending' ? 'PENDIENTE DE REVISIÓN' : 'DENEGADO / INACTIVO'}
{v.status === 'pending' &&
} {v.status === 'approved' && v.type !== 'paid' && }
)) }
)} {managingVacation && (
{setManagingVacation(null); setTempRevokedDates([]);}}>
e.stopPropagation()}>

Edición Quirúrgica

DESMARCA LOS DÍAS ESPECÍFICOS QUE EL USUARIO SÍ TRABAJARÁ (SE LE DEVOLVERÁN A SU SALDO).

{window.getDatesInRange(managingVacation.startDate, managingVacation.endDate || managingVacation.startDate).map(dateStr => { const isRevoked = tempRevokedDates.includes(dateStr); return ( ); })}
)}
{isMaster && ( )}
{tab === 'master' && isMaster && (

Gestión de Portales Corporativos

Crea subdominios y asigna administradores a cada cliente

{masterClients.map(c => (
{c.module_shift == 1 ? 'MÓDULO ACTIVO' : 'INACTIVO'}

{c.company_name}

{c.shift_subdomain}.shift.marcasnicaragua.com

Contacto: {c.contact_name || 'N/A'}

Correo: {c.contact_email}

))}
)} {tab === 'logs' && (
setSearchName(e.target.value)} placeholder="Ej. Prueba 1" className="w-full p-4 bg-slate-50 border border-slate-100 rounded-2xl font-bold outline-none focus:border-indigo-500 transition-colors" />
{setSearchDate(e.target.value); setStartDate(''); setEndDate('');}} className="w-full p-4 bg-slate-50 border border-slate-100 rounded-2xl font-bold outline-none focus:border-indigo-500 transition-colors" />
O Rango de Fechas
{setStartDate(e.target.value); setSearchDate('');}} className="w-full p-2 bg-white rounded-xl font-bold outline-none border border-slate-200 text-xs" /> {setEndDate(e.target.value); setSearchDate('');}} className="w-full p-2 bg-white rounded-xl font-bold outline-none border border-slate-200 text-xs" />
{filteredLogs.map(l => { const actionObj = actions.find(a => a.id === l.type); const displayType = actionObj ? actionObj.label : l.type.replace('_',' '); return ( ) })}
UsuarioMarcaFecha/HoraAcciones
{l.userName} {displayType} {window.formatNICTimeFull ? window.formatNICTimeFull(l.timestamp) : new Date(l.timestamp).toLocaleString()} {l.location && l.location.lat ? ( ) : Sin GPS} {l.photo && setPreviewImage(l.photo)} className="w-10 h-10 rounded-xl object-cover cursor-pointer hover:scale-110 transition-transform border border-slate-200 shadow-sm" />}
)} {tab === 'users' && (

{editingUser ? "Editar Usuario" : "Nuevo Usuario"}

{editingUser && }
{globalUsers.map(u => { const isLocked = isUserLockedToday(u); return (

{u.name}

CÓDIGO/ALIAS: @{u.alias} • ID #{u.id} • {u.role} • {u.status}

{isLocked ? ( ) : ( )}
); })}
)} {tab === 'vacations' && (

Reglas Vacacionales

Tasa maestra de acumulación mensual por cada 30 días naturales trabajados.

setVacRate(e.target.value)} className="w-full p-4 bg-slate-50 rounded-2xl font-bold outline-none focus:ring-2 focus:ring-emerald-100 text-slate-800" />

Solicitudes Entrantes

{vacations.filter(v => v.status === 'pending').length === 0 ? (

Bandeja limpia.

) : ( vacations.filter(v => v.status === 'pending').map(v => (

{v.userName}

Pide: {v.type === 'half' ? 'MEDIO DÍA (0.5)' : `${window.getDaysDiff(v.startDate, v.endDate || v.startDate)} DÍA(S)`}

Del {window.formatDateToNIC?window.formatDateToNIC(v.startDate):v.startDate} {v.type==='range' ? `al ${window.formatDateToNIC?window.formatDateToNIC(v.endDate):v.endDate}`:''}

)) )}

Saldos

TOCA EL NOMBRE PARA GESTIONAR
{globalUsers.map(u => ( setSelectedUserVacation(u)}> ))}
Consultor (Perfil)DisponibleContrato
{u.name} {window.calculateVacationBalance(u, settings?.vacationRate||2.5, vacations)} {window.formatDateToNIC?window.formatDateToNIC(u.hiringDate):u.hiringDate}

Aprobadas General

{vacPage+1} / {totalVacPages||1}
{paginatedVacs.length === 0 ?

Sin registros históricos.

: paginatedVacs.map(v => (

{v.userName}

FECHAS: {window.formatDateToNIC?window.formatDateToNIC(v.startDate):v.startDate} {v.type==='range'?` AL ${window.formatDateToNIC?window.formatDateToNIC(v.endDate):v.endDate}`:''}

{v.revokedDates && v.revokedDates.length > 0 &&

*{v.revokedDates.length} DÍAS REVOCADOS

} {v.type === 'paid' &&

*PAGO EN EFECTIVO ({v.paidDays} DÍAS)

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

Registrar Ausencia

Si seleccionas "Descontar", se restará automáticamente 1 día de las vacaciones del empleado.

setAbsenceDate(e.target.value)} className="w-full p-4 bg-slate-50 rounded-2xl font-bold outline-none focus:ring-2 focus:ring-red-100" required />