const UiIconAdmin = ({ name, className = "w-5 h-5" }) => { const icons = { 'dashboard': , 'users': , 'contracts': , 'shield': , 'file-text': , 'bell': , 'dollar': , 'sun': , 'x-circle': , 'settings': }; return icons[name] || null; }; window.AdminDashboard = function({ currentUser, globalUsers = [], logs = [], roles = [], reminders = [], vacations = [], settings = {}, actions = [], absences = [], masterClients = [], activeClientId, setActiveClientId, refreshData, setView, onLogout }) { const isMaster = window.SHIFT_CONFIG && window.SHIFT_CONFIG.isMaster; // Inicia siempre en Auditoría ('logs') por defecto const [tab, setTab] = React.useState(isMaster && activeClientId === 0 ? 'master' : 'logs'); const [toastMsg, ReactSetToastMsg] = React.useState(''); const [isPunching, setIsPunching] = React.useState(false); // Identificar permisos de Sólo Lectura const currentRoleObj = (roles || []).find(r => (r.name || '').toLowerCase() === (currentUser?.role || currentUser?.role_name || '').toLowerCase()); const isReadOnly = (currentUser?.accessLevel === 'readonly' || currentUser?.accessLevel === 'read_only' || currentRoleObj?.accessLevel === 'readonly' || currentRoleObj?.accessLevel === 'read_only'); const [confirmModal, setConfirmModal] = React.useState(null); const [previewImage, setPreviewImage] = React.useState(null); const [showClientModal, setShowClientModal] = React.useState(false); const [editingClient, setEditingClient] = React.useState(null); const [mapLogs, setMapLogs] = React.useState(null); const mapContainerRef = React.useRef(null); const mapInstanceRef = React.useRef(null); const liveMarkersRef = React.useRef({}); const [editingUser, setEditingUser] = React.useState(null); const [companyLogoUrl, setCompanyLogoUrl] = React.useState(settings?.logoUrl || ''); const [showCompanyLogo, setShowCompanyLogo] = React.useState(settings?.showCompanyLogo !== false); const [inssRate, setInssRate] = React.useState(settings?.inssPatronalRate || 22.5); const [lunchLimitMinutes, setLunchLimitMinutes] = React.useState(settings?.lunchLimitMinutes || 60); const [gracePeriodMinutes, setGracePeriodMinutes] = React.useState(settings?.gracePeriodMinutes || 5); const [maxTardinessAllowed, setMaxTardinessAllowed] = React.useState(settings?.maxTardinessAllowed || 3); const [tardinessPenaltyAction, setTardinessPenaltyAction] = React.useState(settings?.tardinessPenaltyAction || 'deduct_minutes'); const [reasonsList, setReasonsList] = React.useState(settings?.exceptionReasons || ['Horas extras autorizadas', 'Solicitud de gerencia', 'Cobertura de turno especial']); const [newReasonInput, setNewReasonInput] = React.useState(''); const [vacRate, setVacRate] = React.useState(settings?.vacationRate || 2.5); const [selectedUserVacation, setSelectedUserVacation] = React.useState(null); const showToast = (msg) => { ReactSetToastMsg(msg); setTimeout(() => ReactSetToastMsg(''), 3000); }; const confirmAction = (message, action) => { if (isReadOnly) return alert("Modo supervisión: no tienes permisos para realizar esta acción."); setConfirmModal({ message, action }); }; // Último estado de marcaje del Administrador hoy const todayAdminLogs = React.useMemo(() => { const todayStr = window.getNICDate ? window.getNICDate() : new Date().toISOString().split('T')[0]; return (logs || []).filter(l => Number(l.userId) === Number(currentUser?.id) && l.date === todayStr); }, [logs, currentUser]); const lastAdminLog = todayAdminLogs.length > 0 ? todayAdminLogs[0] : null; const isCurrentlyCheckedIn = lastAdminLog && lastAdminLog.type !== 'salida'; // MARCAJE RÁPIDO PARA ADMINS/SUPERVISORES const handleQuickPunch = async (punchType) => { setIsPunching(true); let coords = { lat: 0, lng: 0 }; try { if (navigator.geolocation) { const pos = await new Promise((resolve) => { navigator.geolocation.getCurrentPosition(resolve, () => resolve(null), { timeout: 4000 }); }); if (pos && pos.coords) { coords = { lat: pos.coords.latitude, lng: pos.coords.longitude }; } } } catch (eGeo) {} const now = Date.now(); const dateStr = window.getNICDate ? window.getNICDate() : new Date().toISOString().split('T')[0]; try { const res = await window.apiCall('save_log', { userId: currentUser.id, type: punchType, timestamp: now, date: dateStr, location: coords, client: 'Marcaje Rápido', justification: null }); if (res && res.success) { showToast(`¡${punchType.toUpperCase()} registrada con éxito!`); if (refreshData) await refreshData(); } else { showToast("Error al registrar marcaje: " + (res?.error || "")); } } catch (err) { showToast("Error de conexión al marcar."); } finally { setIsPunching(false); } }; React.useEffect(() => { setCompanyLogoUrl(settings?.logoUrl || ''); setShowCompanyLogo(settings?.showCompanyLogo !== false); setInssRate(settings?.inssPatronalRate || 22.5); setLunchLimitMinutes(settings?.lunchLimitMinutes || 60); setGracePeriodMinutes(settings?.gracePeriodMinutes || 5); setMaxTardinessAllowed(settings?.maxTardinessAllowed || 3); setTardinessPenaltyAction(settings?.tardinessPenaltyAction || 'deduct_minutes'); setReasonsList(settings?.exceptionReasons || ['Horas extras autorizadas', 'Solicitud de gerencia', 'Cobertura de turno especial']); setVacRate(settings?.vacationRate || 2.5); }, [settings]); // MAPA GPS React.useEffect(() => { if (!mapLogs || mapLogs.length === 0 || !mapContainerRef.current) return; if (mapInstanceRef.current) { mapInstanceRef.current.remove(); mapInstanceRef.current = null; } liveMarkersRef.current = {}; const validCoords = mapLogs.filter(l => l.location && l.location.lat && l.location.lng); if (validCoords.length === 0) { showToast("Los registros seleccionados no contienen coordenadas GPS."); setMapLogs(null); return; } const initialPoint = [validCoords[0].location.lat, validCoords[0].location.lng]; const map = L.map(mapContainerRef.current).setView(initialPoint, 14); mapInstanceRef.current = map; L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap' }).addTo(map); const latLngs = []; const sortedPoints = [...validCoords].sort((a, b) => a.timestamp - b.timestamp); sortedPoints.forEach((log, index) => { const pos = [log.location.lat, log.location.lng]; latLngs.push(pos); const isStart = log.type === 'entrada'; const isEnd = log.type === 'salida'; const color = isStart ? '#10b981' : isEnd ? '#ef4444' : '#6366f1'; const userInitial = (log.userName || 'U')[0].toUpperCase(); const avatarHtml = log.avatarUrl ? `
` : `
${userInitial}
`; const customIcon = L.divIcon({ html: avatarHtml, className: 'custom-avatar-marker', iconSize: [38, 38], iconAnchor: [19, 19] }); const marker = L.marker(pos, { icon: customIcon }).addTo(map); const timeStr = window.formatNICTimeFull ? window.formatNICTimeFull(log.timestamp) : new Date(log.timestamp).toLocaleTimeString(); marker.bindPopup(`
${log.userName || 'Colaborador'}
#${index + 1} • ${log.type.toUpperCase()}
Hora: ${timeStr}
`); liveMarkersRef.current[log.id] = marker; }); if (latLngs.length > 1) { L.polyline(latLngs, { color: '#4f46e5', weight: 4, opacity: 0.75, dashArray: '8, 8' }).addTo(map); map.fitBounds(L.latLngBounds(latLngs), { padding: [40, 40] }); } setTimeout(() => { if (mapInstanceRef.current) mapInstanceRef.current.invalidateSize(); }, 300); }, [mapLogs]); const handlePortalChange = (newId) => { const idNum = Number(newId); if (setActiveClientId) setActiveClientId(idNum); if (idNum === 0) setTab('master'); else if (tab === 'master') setTab('logs'); }; const handleUserSubmit = async (e) => { e.preventDefault(); if (isReadOnly) return showToast("Modo supervisión: no tienes permisos de edición."); 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') || 'activo', hiringDate: f.get('date') || window.getNICDate(), vacationDaysUsed: parseFloat(f.get('vused')) || 0, scheduleDays: JSON.parse(f.get('scheduleDays') || '["lun","mar","mie","jue","vie"]'), scheduleStart: f.get('schedule_start') || '08:00', scheduleEnd: f.get('schedule_end') || '17:00', scheduleCustom: JSON.parse(f.get('scheduleCustom') || '{}'), hasInss: f.get('hasInss') === '1' || f.get('hasInss') === 1, inssNumber: f.get('inssNumber') || null, baseSalary: parseFloat(f.get('base_salary')) || 0, appliesInss: f.get('applies_inss') ? 1 : 0, appliesIr: f.get('applies_ir') ? 1 : 0, gpsTrackingEnabled: f.get('gps_tracking_enabled') ? 1 : 0, avatar_base64: f.get('avatar_base64') || null, idCard: f.get('id_card') || null, civilStatus: f.get('civil_status') || 'Soltero(a)', profession: f.get('profession') || 'Trabajador(a)', homeAddress: f.get('home_address') || null, positionTitle: f.get('position_title') || null, paymentFrequency: f.get('payment_frequency') || 'Quincenal', overrideCheckOut: editingUser ? (editingUser.overrideCheckOut || false) : false, manualLock: editingUser ? (editingUser.manualLock || false) : false }; try { const res = await window.apiCall('save_user', data); if(res.success) { setEditingUser(null); showToast("Usuario guardado exitosamente."); if (refreshData) await refreshData(); } else { showToast("Error al guardar: " + (res.error || "Desconocido")); } } catch (err) { showToast("Error de conexión al guardar usuario."); } }; const deleteUser = async (id) => { if (isReadOnly) return showToast("Modo supervisión: no tienes permisos para eliminar."); await window.apiCall('delete_user', { id }); setConfirmModal(null); showToast("Usuario borrado."); if (refreshData) await refreshData(); }; const toggleLock = async (u, lockStatus) => { if (isReadOnly) return showToast("Modo supervisión: no tienes permisos de bloqueo."); await window.apiCall('toggle_user_lock', { id: u.id, manualLock: lockStatus, overrideCheckOut: !lockStatus }); showToast(lockStatus ? "Bloqueado." : "Desbloqueado."); if (refreshData) await refreshData(); }; const saveCompanySettings = async (extraPayload = {}) => { if (isReadOnly) return showToast("Modo supervisión: no tienes permisos de configuración."); const res = await window.apiCall('update_settings', { vacation_rate: parseFloat(vacRate) || 2.5, exception_reasons: reasonsList, inss_patronal_rate: parseFloat(inssRate) || 22.5, lunch_limit_minutes: parseInt(lunchLimitMinutes) || 60, grace_period_minutes: parseInt(gracePeriodMinutes) || 5, max_tardiness_allowed: parseInt(maxTardinessAllowed) || 3, tardiness_penalty_action: tardinessPenaltyAction || 'deduct_minutes', show_company_logo: showCompanyLogo ? 1 : 0, ...extraPayload }); if (res.success && res.logo_url) { setCompanyLogoUrl(res.logo_url); } showToast("Configuración de empresa actualizada."); if (refreshData) await refreshData(); }; const exportPayrollReport = () => { const headers = ["ID", "Empleado", "Correo", "Salario Base (C$)", "Faltas Deducidas", "Días Efectivos", "Salario Bruto", "INSS Laboral (7%)", "IR DGI", "Salario Neto", "INSS Patronal"]; const rows = globalUsers.map(u => { const calc = window.calculatePayrollRow(u, 30, 0, settings?.inssPatronalRate || 22.5, absences); return [u.id, u.name, u.email, u.baseSalary || 0, calc.deductedDays, calc.effectiveWorkedDays, calc.grossSalary, calc.inssLaboral, calc.irDGI, calc.netSalary, calc.inssPatronal]; }); window.exportToCSV(`Planilla_${window.SHIFT_CONFIG?.companyName || 'Empresa'}_${window.getNICDate()}`, headers, rows); }; const setVacationStatus = async (vId, status) => { if (isReadOnly) return showToast("Modo supervisión: no puedes cambiar estados."); await window.apiCall('update_vacation_status', { id: vId, status }); showToast(`Actualizado a: ${status}`); if (refreshData) await refreshData(); }; const revokeEntireVacation = async (vac) => { if (isReadOnly) return showToast("Modo supervisión: no tienes permisos de revocación."); confirmAction(`¿Revocar vacaciones de ${vac.userName}? Se reintegrarán sus días.`, async () => { await window.apiCall('delete_vacation', { id: vac.id }); setConfirmModal(null); showToast("Vacaciones revocadas con éxito."); if (refreshData) await refreshData(); }); }; const calculateUsedDaysBreakdown = (user) => { if (!user) return 0; const manualUsed = parseFloat(user.vacationDaysUsed) || 0; const systemApproved = (vacations || []) .filter(v => v.userId === user.id && v.status === 'approved') .reduce((acc, v) => { if (v.type === 'paid') return acc + (v.paidDays || 0); const totalReq = v.type === 'half' ? 0.5 : window.getDaysDiff(v.startDate, v.endDate || v.startDate); const revoked = v.revokedDates ? v.revokedDates.length : 0; return acc + Math.max(0, totalReq - revoked); }, 0); return (manualUsed + systemApproved).toFixed(2); }; const renderNavItems = () => { if (isMaster && activeClientId === 0) { return ( ); } return ( {!isReadOnly && ( )} {!isReadOnly && ( )} ); }; return (
{toastMsg &&
{toastMsg}
} {previewImage && (
setPreviewImage(null)}>
e.stopPropagation()}>
)} {mapLogs && (
setMapLogs(null)}>
e.stopPropagation()}>

Ruta y Marcajes GPS

{mapLogs.length} punto(s) registrado(s) • Monitoreo interactivo

)} {confirmModal && (
setConfirmModal(null)}>
e.stopPropagation()}>

Confirmación

{confirmModal.message}

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

{selectedUserVacation.name}

REPORTE VACACIONAL • INICIO: {window.formatDateToNIC(selectedUserVacation.hiringDate)}

Acumuladas Ley

{window.calculateVacationAccrued(selectedUserVacation, settings?.vacationRate||2.5).toFixed(2)}

Gozados / Aprobados

{calculateUsedDaysBreakdown(selectedUserVacation)}

Saldo Disponible

{window.calculateVacationBalance(selectedUserVacation, settings?.vacationRate||2.5, vacations)}

)} {/* BARRA LATERAL CON MARCAJE RÁPIDO INTEGRADO */}
{tab === 'master' && isMaster && window.AdminSaaS && ( )} {tab === 'logs' && window.AdminAuditoria && ( )} {tab === 'users' && window.AdminPersonal && ( )} {tab === 'contracts' && window.AdminContratos && ( )} {tab === 'notices' && window.AdminAvisos && ( )} {tab === 'payroll' && window.AdminPlanilla && ( )} {tab === 'vacations' && window.AdminVacaciones && ( { setVacRate(rate); saveCompanySettings(); }} setVacationStatus={setVacationStatus} revokeEntireVacation={revokeEntireVacation} setSelectedUserVacation={setSelectedUserVacation} isReadOnly={isReadOnly} /> )} {tab === 'absences' && window.AdminAusencias && ( {}} confirmAction={confirmAction} isReadOnly={isReadOnly} showToast={showToast} /> )} {!isReadOnly && tab === 'roles' && window.AdminRoles && ( )} {!isReadOnly && tab === 'settings' && window.AdminAjustes && ( )}
{/* ASISTENTE INTELIGENTE COPILOT AI (WIDGET FLOTANTE MULTI-TENANT) */} {window.AdminAICopilot && ( )}
); };